家計簿アプリ 開発ガイド
目次
完成イメージ
- 収入・支出をタイトル・金額・種類で入力できる
- 入力したデータが一覧で表示される
- 収入合計・支出合計・残高が自動計算される
- 円グラフ・棒グラフで収支が見える化される
- ページを更新してもデータが消えない(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.jsxTransactionList.jsxSummary.jsxCharts.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) // 文字列 → 数値に変換