Reactで作るTODOアプリ開発シリーズ第6回
この記事では、
「編集」と「削除」機能
を追加し、基本的なCRUD操作を完成させます。
前回はリスト表示と条件付きレンダリングについて学習しました。今回は、ユーザーがTODOを編集・削除できるようにし、より実用的なアプリに仕上げていきます。
- 配列のfilter/mapによるデータ操作
- 編集・削除機能の実装方法
- 編集モードの切り替え(条件付きレンダリング)
- キーボードイベントの活用
- UI/UX向上のための工夫

編集・削除機能の実装方法を理解することで、CRUD操作を含めたReactアプリの開発が行えるようになります!






TODOアプリ開発シリーズのまとめ記事はこちら
前回までのコード
第5回まで作成・修正したファイル構成と主要なコードは以下の通りです。
ファイル構成
todo-app/
├── src/
│ ├── App.js
│ ├── App.css
│ ├── TodoItem.js
│ ├── TodoList.js
│ ├── TodoHeader.js
│ ├── TodoForm.js
│ └── TodoFilter.js
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');
// フィルタリングされたTODOリスト
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;
// TODOの完了状態を切り替える関数
const toggleTodo = (id) => {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
)
);
};
// 新しいTODOを追加する関数
const addTodo = (newTodo) => {
setTodos(prevTodos => [...prevTodos, newTodo]);
};
return (
<div className="App">
<TodoHeader
title="React TODOアプリ"
totalCount={todos.length}
completedCount={completedCount}
/>
<TodoForm onAddTodo={addTodo} />
<TodoFilter filter={filter} onFilterChange={setFilter} />
<TodoList todos={filteredTodos} onToggle={toggleTodo} />
</div>
);
}
export default App;
TodoItem.js
import React from 'react';
function TodoItem({ id, text, completed, priority = "medium", onToggle }) {
return (
<div className={`todo-item priority-${priority}`}>
<input
type="checkbox"
checked={completed}
onChange={() => onToggle(id)}
/>
<span style={{ textDecoration: completed ? 'line-through' : 'none' }}>{text}</span>
{completed && (
<span className="completed-badge">完了</span>
)}
{!completed && (
<span className="pending-badge">未完了</span>
)}
</div>
);
}
export default TodoItem;
TodoList.js
import React from 'react';
import TodoItem from './TodoItem';
function TodoList({ todos, onToggle }) {
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}
/>
))}
</div>
);
}
export default TodoList;
TodoFilter.js
import React from 'react';
function TodoFilter({ filter, onFilterChange }) {
return (
<div className="todo-filter">
<button
className={`filter-button ${filter === 'all' ? 'active' : ''}`}
onClick={() => onFilterChange('all')}
>
すべて
</button>
<button
className={`filter-button ${filter === 'pending' ? 'active' : ''}`}
onClick={() => onFilterChange('pending')}
>
未完了
</button>
<button
className={`filter-button ${filter === 'completed' ? 'active' : ''}`}
onClick={() => onFilterChange('completed')}
>
完了済み
</button>
</div>
);
}
export default TodoFilter;
App.css
.todo-filter {
display: flex;
justify-content: center;
gap: 10px;
margin-bottom: 20px;
}
.filter-button {
padding: 8px 16px;
border: 1px solid #ddd;
background-color: white;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.filter-button:hover {
background-color: #f8f9fa;
}
.filter-button.active {
background-color: #007bff;
color: white;
border-color: #007bff;
}
.empty-state {
text-align: center;
padding: 40px;
color: #666;
font-style: italic;
}
.completed-badge {
background-color: #28a745;
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
margin-left: 10px;
}
.pending-badge {
background-color: #ffc107;
color: #212529;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
margin-left: 10px;
}
編集・削除機能の実装に必要な前提知識
編集・削除機能を実装する前に、必要な前提知識を整理しておきましょう。
配列操作の基礎
編集・削除機能では、配列のfilter
とmap
メソッドを活用します。
filterメソッド
配列から条件に合う要素のみを抽出するメソッドです。
// 削除機能で使用:指定したID以外のTODOを残す
const newTodos = todos.filter(todo => todo.id !== idToDelete);
mapメソッド
配列の各要素を変換するメソッドです。
// 編集機能で使用:指定したIDのTODOのテキストを更新
const newTodos = todos.map(todo =>
todo.id === idToEdit
? { ...todo, text: newText }
: todo
);
イベントの伝播と停止
編集モードでは、キーボードイベント(Enter、Escape)を活用します。
これにより、ユーザーはマウスを使用しなくてもTODOの追加・編集・キャンセルができるようになるため、ユーザビリティの向上につながります。
onKeyDownイベント
キーが押された瞬間に発火するイベントです。
const handleKeyDown = (event) => {
if (event.key === 'Enter') {
// 保存処理
}
if (event.key === 'Escape') {
// キャンセル処理
}
};
条件付きレンダリングの応用
編集モードと表示モードを切り替えるために、条件付きレンダリングを活用します。
{isEditing ? (
// 編集モードのUI
) : (
// 表示モードのUI
)}
【実践】編集・削除機能の実装
ここからは、実際に手を動かして、TODOの編集・削除機能を追加していきましょう。
1. TodoItem.jsの修正
既存のsrc/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;
2. TodoList.jsの修正
既存のsrc/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;
3. App.jsの修正
既存のsrc/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;
4. App.cssの追加スタイル
src/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;
}
動作確認
実装が完了したら、開発サーバーが起動している状態でブラウザを確認してください。以下の機能が動作することを確認できます。






- 編集ボタンで編集モードに切り替え、保存・キャンセルができる
- 削除ボタンでTODOが削除できる
- Enterキーで保存、Escapeキーでキャンセル
- 編集・削除後もフィルタリングが正常に動作する
発展課題
基本機能を理解したら、ぜひ以下の課題に挑戦してみてください!
- 削除確認ダイアログの実装
- 一括削除機能の追加
- 編集履歴の保存・元に戻す機能
FAQ
まとめ
今回は、React TODOアプリに編集・削除機能を追加し、基本的なCRUD操作を完成させました。
- filterメソッドによる削除
- mapメソッドによる編集
- 条件付きレンダリングによる編集モードの切り替え
- キーボードイベントの活用
- 状態管理の応用
ここまでお疲れさまでした! 次回はいよいよ「基本編」の最終回です!



基本編の最後では、ローカルストレージを使用してデータを永続化させます。これによりブラウザを更新してもTODOリストの内容がリセットされないようになります!
データベースを使用せず簡単に実装できる内容です。ローカルストレージを使用できれば実装できることの幅が大きく広がります。次回もどうぞお楽しみに!
コメント