「ここまで学んだReactの知識を、実際のアプリケーション開発に活かしたい!」
useStateでの状態管理、イベントハンドリング、フォームの実装など、これまで個別に学んできた知識を組み合わせて、実際のアプリケーションを作ってみましょう。
今回は、シンプルなTodoアプリの開発を通じて、Reactの基本的な機能を総復習します。V0とCursorを活用しながら、CRUD(作成・読取・更新・削除)機能を持つアプリケーションの実装方法を学んでいきましょう。
シンプルなTodoアプリを作る:React基礎の総復習
「ここまで学んだReactの知識を、実際のアプリケーション開発に活かしたい!」
useStateでの状態管理、イベントハンドリング、フォームの実装など、これまで個別に学んできた知識を組み合わせて、実際のアプリケーションを作ってみましょう。
今回は、シンプルなTodoアプリの開発を通じて、Reactの基本的な機能を総復習します。V0とCursorを活用しながら、CRUD(作成・読取・更新・削除)機能を持つアプリケーションの実装方法を学んでいきましょう。
Step 1: V0でベース実装を生成
まず、V0に以下のように依頼して、基本的なTodoアプリを生成します:
シンプルなTodoアプリを作りたいです:
- タスクの追加、表示機能
- TailwindCSSでスタイリング
- useStateを使った状態管理
Reactコンポーネントを作成してください。
V0は以下のようなコードを生成してくれました:
jsx
import React, { useState } from 'react';
const TodoApp = () => {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (!input.trim()) return;
setTodos([...todos, {
id: Date.now(),
text: input,
completed: false
}]);
setInput('');
};
return (
<div className="max-w-md mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">Todoリスト</h1>
<form onSubmit={handleSubmit} className="mb-4">
<div className="flex gap-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
className="flex-1 px-3 py-2 border rounded"
placeholder="新しいタスクを入力..."
/>
<button
type="submit"
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
追加
</button>
</div>
</form>
<ul className="space-y-2">
{todos.map(todo => (
<li
key={todo.id}
className="flex items-center p-3 border rounded"
>
<span>{todo.text}</span>
</li>
))}
</ul>
</div>
);
};
export default TodoApp;
Step 2: Cursorで機能を改善
基本的な実装ができたところで、Cursorに以下のように依頼して機能を追加していきます:
このTodoアプリに以下の機能を追加したいです:
1. タスクの完了状態の切り替え
2. 削除機能
3. 編集機能
実装方法を提案してください。
Cursorの提案を取り入れた改善版はこちらです:
jsx
import React, { useState } from 'react';
const TodoApp = () => {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
const [editingId, setEditingId] = useState(null);
const [editInput, setEditInput] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (!input.trim()) return;
setTodos([...todos, {
id: Date.now(),
text: input,
completed: false
}]);
setInput('');
};
const toggleComplete = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
const startEditing = (todo) => {
setEditingId(todo.id);
setEditInput(todo.text);
};
const handleEdit = (id) => {
if (!editInput.trim()) return;
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, text: editInput } : todo
));
setEditingId(null);
setEditInput('');
};
return (
<div className="max-w-md mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">Todoリスト</h1>
<form onSubmit={handleSubmit} className="mb-4">
<div className="flex gap-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
className="flex-1 px-3 py-2 border rounded shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="新しいタスクを入力..."
/>
<button
type="submit"
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
>
追加
</button>
</div>
</form>
<ul className="space-y-2">
{todos.map(todo => (
<li
key={todo.id}
className="flex items-center p-3 border rounded hover:shadow-md transition-shadow"
>
{editingId === todo.id ? (
<div className="flex items-center gap-2 flex-1">
<input
type="text"
value={editInput}
onChange={(e) => setEditInput(e.target.value)}
className="flex-1 px-2 py-1 border rounded"
autoFocus
/>
<button
onClick={() => handleEdit(todo.id)}
className="px-3 py-1 text-sm text-white bg-green-500 rounded hover:bg-green-600"
>
保存
</button>
<button
onClick={() => setEditingId(null)}
className="px-3 py-1 text-sm text-gray-600 hover:text-gray-800"
>
キャンセル
</button>
</div>
) : (
<>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleComplete(todo.id)}
className="mr-3"
/>
<span className={`flex-1 ${todo.completed ? 'line-through text-gray-400' : ''}`}>
{todo.text}
</span>
<button
onClick={() => startEditing(todo)}
className="px-2 text-gray-500 hover:text-gray-700"
>
編集
</button>
<button
onClick={() => deleteTodo(todo.id)}
className="px-2 text-red-500 hover:text-red-700"
>
削除
</button>
</>
)}
</li>
))}
</ul>
{todos.length === 0 && (
<p className="text-center text-gray-500 mt-4">
タスクがありません。新しいタスクを追加してください。
</p>
)}
</div>
);
};
export default TodoApp;
Step 3: 機能の拡張
さらに、Cursorに以下のように依頼して機能を拡張します:
追加の機能を実装したいです:
1. フィルター機能(全て/未完了/完了済み)
2. ローカルストレージへの保存
3. タスクの並び替え
実装方法を提案してください。
jsx
import React, { useState, useEffect } from 'react';
const TodoApp = () => {
const [todos, setTodos] = useState(() => {
// ローカルストレージからの初期データ読み込み
const savedTodos = localStorage.getItem('todos');
return savedTodos ? JSON.parse(savedTodos) : [];
});
const [input, setInput] = useState('');
const [editingId, setEditingId] = useState(null);
const [editInput, setEditInput] = useState('');
const [filter, setFilter] = useState('all');
const [sortBy, setSortBy] = useState('date');
// ローカルストレージへの保存
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
const handleSubmit = (e) => {
e.preventDefault();
if (!input.trim()) return;
setTodos([...todos, {
id: Date.now(),
text: input,
completed: false,
createdAt: new Date().toISOString()
}]);
setInput('');
};
const toggleComplete = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id) => {
if (window.confirm('このタスクを削除してもよろしいですか?')) {
setTodos(todos.filter(todo => todo.id !== id));
}
};
const startEditing = (todo) => {
setEditingId(todo.id);
setEditInput(todo.text);
};
const handleEdit = (id) => {
if (!editInput.trim()) return;
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, text: editInput } : todo
));
setEditingId(null);
setEditInput('');
};
// フィルタリングされたTodoの取得
const getFilteredTodos = () => {
let filtered = [...todos];
// フィルター適用
switch (filter) {
case 'active':
filtered = filtered.filter(todo => !todo.completed);
break;
case 'completed':
filtered = filtered.filter(todo => todo.completed);
break;
default:
break;
}
// ソート適用
switch (sortBy) {
case 'date':
filtered.sort((a, b) => b.id - a.id);
break;
case 'alphabetical':
filtered.sort((a, b) => a.text.localeCompare(b.text));
break;
case 'completed':
filtered.sort((a, b) => Number(a.completed) - Number(b.completed));
break;
default:
break;
}
return filtered;
};
const filteredTodos = getFilteredTodos();
return (
<div className="max-w-md mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">Todoリスト</h1>
<form onSubmit={handleSubmit} className="mb-4">
<div className="flex gap-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
className="flex-1 px-3 py-2 border rounded shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="新しいタスクを入力..."
/>
<button
type="submit"
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
>
追加
</button>
</div>
</form>
<div className="flex justify-between mb-4">
<div className="space-x-2">
<select
value={filter}
onChange={(e) => setFilter(e.target.value)}
className="px-2 py-1 border rounded"
>
<option value="all">すべて</option>
<option value="active">未完了</option>
<option value="completed">完了済み</option>
</select>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
className="px-2 py-1 border rounded"
>
<option value="date">日付順</option>
<option value="alphabetical">名前順</option>
<option value="completed">完了状態順</option>
</select>
</div>
{todos.length > 0 && (
<button
onClick={() => {
if (window.confirm('すべてのタスクを削除してもよろしいですか?')) {
setTodos([]);
}
}}
className="text-sm text-red-500 hover:text-red-700"
>
すべて削除
</button>
)}
</div>
<ul className="space-y-2">
{filteredTodos.map(todo => (
<li
key={todo.id}
className="flex items-center p-3 border rounded hover:shadow-md transition-shadow"
>
{editingId === todo.id ? (
<div className="flex items-center gap-2 flex-1">
<input
type="text"
value={editInput}
onChange={(e) => setEditInput(e.target.value)}
className="flex-1 px-2 py-1 border rounded"
autoFocus
/>
<button
onClick={() => handleEdit(todo.id)}
className="px-3 py-1 text-sm text-white bg-green-500 rounded hover:bg-green-600"
>
保存
</button>
<button
onClick={() => setEditingId(null)}
className="px-3 py-1 text-sm text-gray-600 hover:text-gray-800"
>
キャンセル
</button>
</div>
) : (
<>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleComplete(todo.id)}
className="mr-3"
/>
<span className={`flex-1 ${todo.completed ? 'line-through text-gray-400' : ''}`}>
{todo.text}
</span>
<button
onClick={() => startEditing(todo)}
className="px-2 text-gray-500 hover:text-gray-700"
>
編集
</button>
<button
onClick={() => deleteTodo(todo.id)}
className="px-2 text-red-500 hover:text-red-700"
>
削除
</button>
</>
)}
</li>
))}
</ul>
{todos.length === 0 && (
<p className="text-center text-gray-500 mt-4">
タスクがありません。新しいタスクを追加してください。
</p>
)}
</div>
);
};
export default TodoApp;
実装のポイント解説
1. 状態管理の設計
- 複数の状態を適切に分割(todos, input, editingId, filter, sortBy)
- ローカルストレージとの連携による永続化
- フィルタリングとソート機能の実装
2. イベントハンドリング
- フォーム送信(handleSubmit)
- タスクの操作(toggleComplete, deleteTodo)
- 編集モードの切り替え(startEditing, handleEdit)
3. UIの改善
- フィルターとソートのドロップダウン
- 編集モードの切り替えアニメーション
- レスポンシブデザイン
4. ユーザビリティの向上
- 確認ダイアログの追加
- エラー処理の実装
- ローディング状態の表示
コードの再利用性を高める工夫
V0で生成したコードを改善する際、以下のような点に注目しました:
- コンポーネントの分割
- TodoItemコンポーネント
- TodoFormコンポーネント
- FilterControlsコンポーネント
- カスタムフックの作成
- useLocalStorage
- useTodoFilters
- useTodoActions
まとめ
今回は、V0とCursorを活用してTodoアプリを段階的に実装しました:
- V0で基本的な機能を生成
- Cursorで機能を拡張
- UIとUXを改善
- データの永続化を実装