2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Reactの学習がてら簡単な家計簿アプリを作成してみた

2
Posted at

家計簿アプリ 開発ガイド

目次

  1. 完成イメージ
  2. 使用技術
  3. 環境構築
  4. フォルダ構成
  5. 実装手順
  6. つまずきポイント集

完成イメージ

  • 収入・支出をタイトル・金額・種類で入力できる
  • 入力したデータが一覧で表示される
  • 収入合計・支出合計・残高が自動計算される
  • 円グラフ・棒グラフで収支が見える化される
  • ページを更新してもデータが消えない(localStorage保存)
  • 一覧から取引を削除できる

使用技術

技術 用途
React UIライブラリ
Vite プロジェクト作成・開発サーバー
Bootstrap CSSライブラリ(デザイン)
Recharts グラフライブラリ

環境構築

1. プロジェクト作成

npm create vite@latest kakeibo-app -- --template react
cd kakeibo-app

2. ライブラリのインストール

npm install bootstrap
npm install recharts

3. 開発サーバーの起動

npm run dev

ブラウザで http://localhost:5173/ を開いて確認できます。

⚠️ 注意
PCを再起動したり、PowerShellを閉じた後は毎回 npm run dev が必要です。


フォルダ構成

kakeibo-app/
├── src/
│   ├── components/
│   │   ├── Charts.jsx          # グラフ
│   │   ├── Summary.jsx         # 合計・残高
│   │   ├── TransactionForm.jsx # 入力フォーム
│   │   └── TransactionList.jsx # 一覧リスト
│   ├── App.jsx                 # メインコンポーネント
│   ├── TransactionContext.jsx  # データ管理(Context・Reducer)
│   ├── main.jsx                # アプリの起動ファイル
│   └── index.css               # グローバルCSS
├── package.json
└── vite.config.js

実装手順

Step1: Bootstrapの導入

src/main.jsx を以下に書き換えます。

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import 'bootstrap/dist/css/bootstrap.min.css'
import './index.css'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

ポイント

  • import 'bootstrap/dist/css/bootstrap.min.css' を追加するだけでBootstrapが使えるようになります

Step2: TransactionContextの作成

src/TransactionContext.jsx を新規作成します。

import { createContext, useContext, useReducer, useEffect } from 'react'

const TransactionContext = createContext(null)

// reducer関数:追加・削除の処理をまとめる
const reducer = (state, action) => {
  switch (action.type) {
    case 'ADD':
      return [...state, action.payload]
    case 'DELETE':
      return state.filter((t) => t.id !== action.payload)
    default:
      return state
  }
}

// Providerコンポーネント:データを子コンポーネントに提供する
export function TransactionProvider({ children }) {
  const [transactions, dispatch] = useReducer(reducer, [])

  // ページを開いたときlocalStorageからデータを読み込む
  useEffect(() => {
    const saved = localStorage.getItem('transactions')
    if (saved) {
      JSON.parse(saved).forEach((t) => {
        dispatch({ type: 'ADD', payload: t })
      })
    }
  }, [])

  // transactionsが変わるたびにlocalStorageに保存する
  useEffect(() => {
    if (transactions.length === 0) return
    localStorage.setItem('transactions', JSON.stringify(transactions))
  }, [transactions])

  return (
    <TransactionContext.Provider value={{ transactions, dispatch }}>
      {children}
    </TransactionContext.Provider>
  )
}

// useTransactions:どこからでもtransactionsとdispatchを取得できる関数
export function useTransactions() {
  return useContext(TransactionContext)
}

ポイント

  • useReducer で追加・削除の処理をまとめて管理します
  • useContext でどのコンポーネントからでもデータを取得できます
  • localStorageの保存・読み込みもここで管理します

Step3: TransactionFormの作成

src/components/TransactionForm.jsx を新規作成します。

import { useState, useRef, useCallback } from 'react'
import { useTransactions } from '../TransactionContext'

function TransactionForm() {
  // 入力フォームの状態管理(文字を管理するから空文字が初期値)
  const [title, setTitle] = useState('')
  const [amount, setAmount] = useState('')
  const [type, setType] = useState('支出')

  // タイトル入力欄への参照(最初はまだどこも参照していないのでnull)
  const titleRef = useRef(null)

  // useContextからdispatchを取得
  const { dispatch } = useTransactions()

  // 追加ボタンを押したときの処理
  const handleAdd = useCallback(() => {
    // 新しい取引データをオブジェクトとして作る
    const newTransaction = {
      id: Date.now(),        // 時刻を数字にしたもの。かぶらないIDとして使う
      title: title,          // タイトルの入力値
      amount: Number(amount), // 数字に変換(inputの値は文字列で保存されるため)
      type: type,            // 種類(収入・支出)
    }

    // dispatchでADD命令を送る
    dispatch({ type: 'ADD', payload: newTransaction })

    // 入力欄をリセットする
    setTitle('')
    setAmount('')
    setType('支出')

    // タイトル入力欄にフォーカスを当てる(useRefのDOM操作)
    titleRef.current.focus()
  }, [title, amount, type, dispatch])

  return (
    <div className="mt-4">
      <div className="mb-3">
        <label className="form-label">タイトル</label>
        <input
          type="text"
          className="form-control"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          placeholder="食費"
          ref={titleRef}
        />
      </div>

      <div className="mb-3">
        <label className="form-label">金額</label>
        <div className="input-group mb-3">
          <input
            type="number"
            className="form-control"
            value={amount}
            onChange={(e) => setAmount(e.target.value)}
            placeholder="1000"
          />
          <span className="input-group-text">円</span>
        </div>
      </div>

      <div className="mb-3">
        <label className="form-label">種類</label>
        <select
          className="form-select"
          value={type}
          onChange={(e) => setType(e.target.value)}
        >
          <option value="支出">支出</option>
          <option value="収入">収入</option>
        </select>
      </div>

      <button className="btn btn-primary" onClick={handleAdd}>
        追加
      </button>
    </div>
  )
}

export default TransactionForm

Step4: Summaryの作成

src/components/Summary.jsx を新規作成します。

// propsで親コンポーネントから合計・残高を受け取る
function Summary({ totalIncome, totalExpense, balance }) {
  return (
    <div className="mt-4">
      <p>収入合計:{totalIncome}円</p>
      <p>支出合計:{totalExpense}円</p>
      <p>残高:{balance}円</p>
    </div>
  )
}

export default Summary

Step5: TransactionListの作成

src/components/TransactionList.jsx を新規作成します。

// propsでtransactions(一覧)とonDelete(削除関数)を受け取る
function TransactionList({ transactions, onDelete }) {
  return (
    <ul className="mt-4 list-group">
      {transactions.map((t) => (
        <li
          key={t.id}
          className="list-group-item d-flex justify-content-between align-items-center"
        >
          <span>{t.title} / {t.amount}円 / {t.type}</span>
          {/* クリックしたらその行のidをonDeleteに渡す */}
          <button
            className="btn btn-danger btn-sm"
            onClick={() => onDelete(t.id)}
          >
            削除
          </button>
        </li>
      ))}
    </ul>
  )
}

export default TransactionList

Step6: Chartsの作成

src/components/Charts.jsx を新規作成します。

import {
  PieChart, Pie, Cell, Legend, Tooltip,
  BarChart, Bar, XAxis, YAxis, CartesianGrid
} from 'recharts'

// propsでchartData(グラフ用データ)を受け取る
function Charts({ chartData }) {
  return (
    <div>
      {/* 円グラフ */}
      <div className="mt-4">
        <h5>収支グラフ</h5>
        <PieChart width={400} height={400}>
          <Pie
            data={chartData}
            dataKey="value"
            nameKey="name"
            cx="50%"
            cy="50%"
            outerRadius={150}
          >
            <Cell fill="#28a745" />{/* 収入:緑 */}
            <Cell fill="#dc3545" />{/* 支出:赤 */}
          </Pie>
          <Legend />
          <Tooltip />
        </PieChart>
      </div>

      {/* 棒グラフ */}
      <div className="mt-4">
        <h5>収支棒グラフ</h5>
        <BarChart width={400} height={300} data={chartData}>
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis dataKey="name" />
          <YAxis />
          <Tooltip />
          <Legend />
          <Bar dataKey="value">
            <Cell fill="#28a745" />{/* 収入:緑 */}
            <Cell fill="#dc3545" />{/* 支出:赤 */}
          </Bar>
        </BarChart>
      </div>
    </div>
  )
}

export default Charts

Step7: App.jsxの作成

src/App.jsx を以下に書き換えます。

import { useMemo, useCallback } from 'react'
import { TransactionProvider, useTransactions } from './TransactionContext'
import TransactionForm from './components/TransactionForm'
import Summary from './components/Summary'
import TransactionList from './components/TransactionList'
import Charts from './components/Charts'

// AppはProviderで包むだけ
export default function App() {
  return (
    <TransactionProvider>
      <Main />
    </TransactionProvider>
  )
}

// Mainが実際の表示を担当
function Main() {
  const { transactions, dispatch } = useTransactions()

  // 収入合計(transactionsが変わったときだけ再計算)
  const totalIncome = useMemo(() => {
    return transactions
      .filter((t) => t.type === '収入')
      .reduce((sum, t) => sum + t.amount, 0)
  }, [transactions])

  // 支出合計(transactionsが変わったときだけ再計算)
  const totalExpense = useMemo(() => {
    return transactions
      .filter((t) => t.type === '支出')
      .reduce((sum, t) => sum + t.amount, 0)
  }, [transactions])

  const balance = totalIncome - totalExpense

  // グラフ用データ(Rechartsは { name, value } の形式が必要)
  const chartData = [
    { name: '収入', value: totalIncome },
    { name: '支出', value: totalExpense },
  ]

  // 削除処理(dispatchでDELETE命令を送る)
  const handleDelete = useCallback((id) => {
    dispatch({ type: 'DELETE', payload: id })
  }, [dispatch])

  return (
    <div className="container mt-4">
      <h1>家計簿アプリ</h1>
      <TransactionForm />
      <Summary
        totalIncome={totalIncome}
        totalExpense={totalExpense}
        balance={balance}
      />
      <TransactionList
        transactions={transactions}
        onDelete={handleDelete}
      />
      <Charts chartData={chartData} />
    </div>
  )
}

Step8: main.jsxの修正

Step1で既に修正済みです。確認だけしてください。

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import 'bootstrap/dist/css/bootstrap.min.css'
import './index.css'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

つまずきポイント集

❶ npm run dev を忘れる

症状
ブラウザで「接続が拒否されました」と表示される。

原因
開発サーバーが起動していない。

解決方法

cd kakeibo-app
npm run dev

PCを再起動したり、PowerShellを閉じた後は毎回実行が必要です。


❷ localStorageのデータがリロードで消える

症状
データを追加してページを更新すると消えてしまう。

原因
保存の useEffect が読み込みより先に動いて空のデータで上書きしてしまう。

解決方法
保存の useEffect に「空のときは保存しない」条件を追加する。

useEffect(() => {
  if (transactions.length === 0) return // 空のときは保存しない
  localStorage.setItem('transactions', JSON.stringify(transactions))
}, [transactions])

❸ ファイルが見つからないエラー

症状

Failed to resolve import "./components/TransactionForm"

原因
ファイルが正しいフォルダに入っていない。

解決方法
以下のファイルが src/components/ フォルダの中にあるか確認する。

  • TransactionForm.jsx
  • TransactionList.jsx
  • Summary.jsx
  • Charts.jsx

❹ Reactのインラインスタイルで ::placeholder が使えない

症状

style={{ '::placeholder': { color: '#adb5bd' } }} // 効かない

原因
ReactのインラインスタイルはCSS疑似要素(::placeholder、:hover など)が使えない。

解決方法
index.css に書く。

::placeholder {
  color: #adb5bd !important;
  opacity: 1;
}

❺ useState の値が文字列になっている

症状
金額の計算がおかしい(例:1000 + 2000 = 10002000 になる)。

原因
input の値は文字列として保存されるため、そのまま足し算すると文字列の結合になる。

解決方法
Number() で数値に変換する。

amount: Number(amount) // 文字列 → 数値に変換
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?