Reactで作るTODOアプリ開発シリーズ第7回(基本編 最終回)
いよいよ基本編の最終回となるこの記事では、
ReactのuseEffectフックを使った副作用処理とローカルストレージによるデータ永続化
について学びます。
前回は編集・削除機能を追加し、基本的なCRUD操作を完成させました。今回は、TODOリストの保存や、実用的な副作用処理の基礎を身につけていきます。
これにより、アプリのデータを永続化ができるようになります。
- useEffectフックの基本的な使い方
- 副作用(サイドエフェクト)とは何か
- ローカルストレージへの保存・読み込み

TODOアプリ開発シリーズのまとめ記事はこちら
前回までのコード
第6回までで作成・修正したファイル構成と主要なコードは以下の通りです。
ファイル構成
todo-app/
├── src/
│ ├── App.js
│ ├── App.css
│ ├── TodoItem.js
│ ├── TodoList.js
│ ├── TodoHeader.js
│ ├── TodoForm.js
│ └── TodoFilter.js
TodoItem.js
import React, { useState } from 'react';
function TodoItem({ id, text, completed, priority = "medium", onToggle, onDelete, onEdit }) {
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState(text);
const handleEdit = () => {
setIsEditing(true);
setEditText(text);
};
const handleSave = () => {
if (editText.trim()) {
onEdit(id, editText);
setIsEditing(false);
}
};
const handleCancel = () => {
setIsEditing(false);
setEditText(text);
};
const handleKeyDown = (event) => {
if (event.key === 'Enter') {
handleSave();
}
if (event.key === 'Escape') {
handleCancel();
}
};
return (
<div className={`todo-item priority-${priority}`}>
<input
type="checkbox"
checked={completed}
onChange={() => onToggle(id)}
/>
{isEditing ? (
<div className="edit-section">
<input
type="text"
value={editText}
onChange={(e) => setEditText(e.target.value)}
onKeyDown={handleKeyDown}
className="edit-input"
autoFocus
/>
<button onClick={handleSave} className="save-button">保存</button>
<button onClick={handleCancel} className="cancel-button">キャンセル</button>
</div>
) : (
<div className="todo-content">
<span style={{ textDecoration: completed ? 'line-through' : 'none' }}>{text}</span>
<span className={`priority-badge ${priority}`}>{priority === 'high' ? '高' : priority === 'medium' ? '中' : '低'}</span>
<div className="todo-actions">
<button onClick={handleEdit} className="edit-button">編集</button>
<button onClick={() => onDelete(id)} className="delete-button">削除</button>
</div>
</div>
)}
</div>
);
}
export default TodoItem;
TodoList.js
import React from 'react';
import TodoItem from './TodoItem';
function TodoList({ todos, onToggle, onDelete, onEdit }) {
if (todos.length === 0) {
return (
<div className="empty-state">
<p>TODOがありません。新しいTODOを追加してください。</p>
</div>
);
}
return (
<div className="todo-list">
{todos.map((todo) => (
<TodoItem
key={todo.id}
id={todo.id}
text={todo.text}
completed={todo.completed}
priority={todo.priority}
onToggle={onToggle}
onDelete={onDelete}
onEdit={onEdit}
/>
))}
</div>
);
}
export default TodoList;
App.js
import React, { useState } from 'react';
import './App.css';
import TodoHeader from './TodoHeader';
import TodoList from './TodoList';
import TodoForm from './TodoForm';
import TodoFilter from './TodoFilter';
function App() {
const [todos, setTodos] = useState([
{ id: 1, text: "Reactを学ぶ", completed: false, priority: "high" },
{ id: 2, text: "TODOアプリを作る", completed: true, priority: "medium" },
{ id: 3, text: "データベース連携", completed: false, priority: "low" },
{ id: 4, text: "デプロイする", completed: false, priority: "medium" }
]);
const [filter, setFilter] = useState('all');
const filteredTodos = todos.filter(todo => {
if (filter === 'all') return true;
if (filter === 'completed') return todo.completed;
if (filter === 'pending') return !todo.completed;
return true;
});
const completedCount = todos.filter(todo => todo.completed).length;
const toggleTodo = (id) => {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
)
);
};
const addTodo = (newTodo) => {
setTodos(prevTodos => [...prevTodos, newTodo]);
};
const deleteTodo = (id) => {
setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
};
const editTodo = (id, newText) => {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === id
? { ...todo, text: newText }
: todo
)
);
};
return (
<div className="App">
<TodoHeader
title="React TODOアプリ"
totalCount={todos.length}
completedCount={completedCount}
/>
<TodoForm onAddTodo={addTodo} />
<TodoFilter
filter={filter}
onFilterChange={setFilter}
totalCount={todos.length}
activeCount={todos.filter(todo => !todo.completed).length}
completedCount={completedCount}
/>
<TodoList
todos={filteredTodos}
onToggle={toggleTodo}
onDelete={deleteTodo}
onEdit={editTodo}
/>
</div>
);
}
export default App;
App.css
.edit-section {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
.edit-input {
flex: 1;
padding: 8px 12px;
border: 2px solid #007bff;
border-radius: 4px;
font-size: 14px;
outline: none;
}
.save-button {
padding: 6px 12px;
background-color: #28a745;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.save-button:hover {
background-color: #218838;
}
.cancel-button {
padding: 6px 12px;
background-color: #6c757d;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.cancel-button:hover {
background-color: #5a6268;
}
.todo-actions {
display: flex;
gap: 8px;
margin-left: auto;
}
.edit-button {
padding: 4px 8px;
background-color: #ffc107;
color: #212529;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.edit-button:hover {
background-color: #e0a800;
}
.delete-button {
padding: 4px 8px;
background-color: #dc3545;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.delete-button:hover {
background-color: #c82333;
}
useEffectと副作用の基礎
編集・削除機能を実装する前に、必要な前提知識を整理しておきましょう。
副作用(サイドエフェクト)とは
レンダリング以外の処理(データ取得、ローカルストレージ操作、タイマー設定など)のことを指します。
useEffectの基本構文
import { useEffect } from 'react';
useEffect(() => {
// 副作用処理
return () => {
// クリーンアップ処理(省略可)
};
}, [依存配列]);
- 依存配列が空:マウント時のみ実行
- 依存配列に値:その値が変化したときに実行
Reactにおけるマウントとは、コンポーネントが画面上に初めて表示されるタイミングのことを指します。Reactでは、コンポーネントが生成されてDOMに追加されることを「マウント」と呼びます。
【サンプル】useEffectでコンソールにメッセージを表示
import React, { useEffect } from 'react';
function SampleComponent() {
useEffect(() => {
console.log('コンポーネントがマウントされました');
return () => {
console.log('コンポーネントがアンマウントされました');
};
}, []);
return <div>useEffectのサンプル</div>;
}
ローカルストレージとは?
ローカルストレージは、Webブラウザにデータを保存できる仕組みです。保存したデータはページをリロードしたり、ブラウザを閉じたりしても消えずに残ります。これにより、ユーザーが入力した情報やアプリの状態を永続的に保持することができます。
例えば、TODOリストの内容をローカルストレージに保存しておけば、次回アクセス時にも前回のリストがそのまま表示されるようになります。
保存先と確認方法
- ローカルストレージの保存先は、各ブラウザのlocalStorage領域です。
- Chromeの場合、F12キーで開発者ツールを開き、「Application」タブ → 「Storage」→「Local Storage」→該当ドメインを選択すると、保存されているデータを確認・編集できます。
- 保存キー(例:todos)や値(JSON形式)もここで直接確認できます。
- ローカルストレージには保存容量(一般的に5MB程度)の制限があります。
- 同じドメイン内のページであればデータは共有されますが、他のブラウザや端末とは共有されません。
- セキュリティ上、パスワードや個人情報などの機密データは保存しないようにしましょう。
- ユーザーが手動でローカルストレージを削除することも可能です。
【実践】ローカルストレージへのTODO保存・復元
ここからは、実際に手を動かして、TODOリストをローカルストレージに保存・復元する機能を追加していきましょう。
App.jsの修正
import React, { useState, useEffect } from 'react';
// ...既存のimport
function App() {
const [todos, setTodos] = useState(() => {
// 初回マウント時にローカルストレージから読み込み
const saved = localStorage.getItem('todos');
return saved ? JSON.parse(saved) : [];
});
// ...他のstateや関数
// todosが変化するたびにローカルストレージへ保存
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
// ...JSXはこれまで通り
}
動作確認
実装が完了したら、以下の点を確認してください。




- TODOを追加・編集・削除しても、ページをリロードしても内容が保持される
- ローカルストレージに「todos」というキーでデータが保存されている
FAQ
まとめ
今回は、useEffectフックを使った副作用処理と、ローカルストレージへのTODO保存・復元を実装しました。
- useEffectの基本的な使い方
- 副作用の概念
- ローカルストレージとの連携
- クリーンアップ処理の実装



これでついに「ReactのTODOアプリ開発シリーズの基本編」は最後の記事を迎えました!
ここまで本当にお疲れ様でした!
ここまでの内容で、TODOアプリに必要な最低限の機能は実装することができました。
なお、本シリーズは次回から「発展編」へ突入します。「基本編」で作成したTODOアプリをベースに、API連携やデータベース連携など、さらなる発展的な技術を学習していきます。
「発展編 第1回」となる次回の記事では、CSS-in-JSを活用したスタイリングとUI/UX改善に挑戦していきます。
次回もどうぞお楽しみに!
コメント