Files
nextjs-todo-tutorial/todo-app-curriculum.md
Yuta Sakurai 97d0606a16 TODOアプリ開発カリキュラムを大幅に充実
- 開発環境セットアップセクションを追加(Node.js、VS Code、Git)
- 各Phaseに詳細な解説と日本語参考文献を追加
- コード例を充実させ、「なぜそう書くのか」の説明を追加
- よくあるエラーと対処法、FAQ(よくある質問)セクションを追加
- 段階的な学習目標と確認ポイントを明確化
- トラブルシューティングとデバッグ方法を充実
- パフォーマンス最適化とベストプラクティスまで網羅
2025-07-08 16:11:22 +09:00

1775 lines
52 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TODOアプリ開発カリキュラム (Next.js + TypeScript)
## 概要
このカリキュラムは、JavaScript/TypeScript初学者がNext.js 15 (App Router)を使って実践的なTODOアプリを開発できるようになることを目的としています。
## 学習目標
- React/Next.jsの基本的な概念を理解する
- TypeScriptの型システムに慣れる
- 状態管理の基本を学ぶ
- CRUDアプリケーションの実装パターンを習得する
- モダンなWeb開発のベストプラクティスを身につける
## 前提知識
- HTML/CSSの基礎知識
- JavaScriptの基本文法変数、関数、配列、オブジェクト
- コマンドラインの基本操作
## 開発環境セットアップ
### 必要なツール
1. **Node.js (v18.17以上)**
- [Node.js公式サイト](https://nodejs.org/ja/)からダウンロード
- `node --version`で確認
2. **Visual Studio Code**
- [公式サイト](https://code.visualstudio.com/)からダウンロード
- 推奨拡張機能:
- TypeScript and JavaScript Language Features
- React snippets
- Tailwind CSS IntelliSense
- Prettier - Code formatter
- ESLint
3. **Git**
- [Git公式サイト](https://git-scm.com/)からダウンロード
- `git --version`で確認
### プロジェクトの作成
```bash
# プロジェクトの作成
npx create-next-app@latest my-todo-app --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
# プロジェクトディレクトリに移動
cd my-todo-app
# 開発サーバーの起動
npm run dev
```
**📚 参考文献:**
- [Next.js公式 - Installation](https://nextjs.org/docs/getting-started/installation)
- [Next.js App Router入門](https://zenn.dev/hayato94087/articles/e9712c4ab7a8f1)
## 学習ステップ
### Phase 1: 基礎理解 (2-3時間)
#### 学習目標
- Next.js App Routerの基本構造を理解する
- ReactコンポーネントとJSXの基本を学ぶ
- TypeScriptの型定義の基礎を身につける
#### Step 1.1: プロジェクト構造の理解
**学習内容:**
- `src/app/`ディレクトリの役割
- `page.tsx``layout.tsx`の違い
- TypeScriptファイル.ts/.tsxの意味
**詳細解説:**
```
src/app/
├── layout.tsx # 全ページ共通のレイアウト
├── page.tsx # ホームページ(/
├── globals.css # 全体のスタイル
└── favicon.ico # ファビコン
```
**📚 このステップの参考文献:**
- [Next.js App Router概要](https://nextjs.org/docs/app)
- [ファイルベースルーティング解説](https://zenn.dev/frontendflat/articles/nextjs-app-router-file-based-routing)
#### Step 1.2: 最初のコンポーネント作成
**学習内容:**
- Hello Worldコンポーネントの作成
- JSXの基本文法
- 型定義の初歩stringやnumberの使い方
**💡 詳細な実装手順:**
1. **新しいコンポーネントファイルを作成**
```typescript
// src/app/components/HelloWorld.tsx
export default function HelloWorld() {
return (
<div className="p-4 bg-blue-100 rounded">
<h2 className="text-xl font-bold">Hello World!</h2>
<p className="text-gray-700">Reactコンポーネントの第一歩です</p>
</div>
);
}
```
**解説:**
- `export default function` - このコンポーネントを他のファイルから使えるようにする
- JSXは見た目はHTMLに似ているが、実はJavaScriptの拡張構文
- `className` - HTMLのclassの代わりに使用JavaScriptの予約語と競合を避けるため
2. **コンポーネントを使ってみる**
```typescript
// src/app/page.tsx
import HelloWorld from './components/HelloWorld';
export default function Home() {
return (
<main className="min-h-screen p-8">
<h1 className="text-3xl font-bold mb-4">TODOアプリ</h1>
<HelloWorld />
</main>
);
}
```
**よくあるエラーと対処法:**
- `Cannot find module` → importパスを確認`./components/HelloWorld`
- `'HelloWorld' is not defined` → importし忘れていないか確認
**📚 このステップの参考文献:**
- [React公式 - コンポーネントとプロパティ](https://ja.react.dev/learn/your-first-component)
- [JSX入門](https://zenn.dev/likr/articles/6be53ca64f29aa035f07)
### Phase 2: TODOアプリの骨組み作成 (3-4時間)
#### 学習目標
- React Hooksの基本useStateを理解する
- TypeScriptでの型定義を学ぶ
- フォームの基本的な操作を身につける
#### Step 2.1: UIコンポーネントの作成
**学習内容:**
- TODOリストの表示部分
- TODO入力フォーム
- Tailwind CSSによるスタイリング
**💡 詳細な実装手順:**
```typescript
// src/app/page.tsx
export default function Home() {
return (
<main className="min-h-screen p-8 bg-gray-50">
<div className="max-w-md mx-auto bg-white rounded-lg shadow-md p-6">
<h1 className="text-2xl font-bold text-gray-800 mb-6">TODOアプリ</h1>
{/* 入力フォーム */}
<div className="mb-4">
<input
type="text"
placeholder="TODOを入力してください"
className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button className="w-full mt-2 bg-blue-500 text-white p-3 rounded-lg hover:bg-blue-600 transition-colors">
</button>
</div>
{/* TODOリスト */}
<div className="space-y-2">
<div className="flex items-center p-3 bg-gray-50 rounded-lg">
<input type="checkbox" className="mr-3 h-5 w-5" />
<span className="flex-1">TODO</span>
<button className="text-red-500 hover:text-red-700 ml-2"></button>
</div>
</div>
</div>
</main>
);
}
```
**Tailwind CSSのポイント:**
- `max-w-md mx-auto` - 幅を制限して中央配置
- `focus:ring-2 focus:ring-blue-500` - フォーカス時の装飾
- `hover:bg-blue-600` - ホバー時の背景色変更
- `space-y-2` - 子要素間の垂直間隔
**📚 このステップの参考文献:**
- [Tailwind CSS公式ドキュメント](https://tailwindcss.com/docs)
- [Tailwind CSS入門](https://zenn.dev/kagan/books/tailwindcss-tutorial)
#### Step 2.2: 状態管理の導入
**学習内容:**
- `useState`フックの使い方
- TODOアイテムの型定義
- 状態とUIの連携
**💡 詳細な実装手順:**
```typescript
// src/app/page.tsx
'use client'; // ← 重要!状態管理を使う時は必須
import { useState } from 'react';
// TypeScriptの型定義
type Todo = {
id: number; // 一意の識別子
text: string; // TODOの内容
completed: boolean; // 完了状態
};
export default function Home() {
// useState フックの使い方
const [todos, setTodos] = useState<Todo[]>([]);
const [inputValue, setInputValue] = useState<string>('');
return (
<main className="min-h-screen p-8 bg-gray-50">
<div className="max-w-md mx-auto bg-white rounded-lg shadow-md p-6">
<h1 className="text-2xl font-bold text-gray-800 mb-6">TODOアプリ</h1>
{/* 入力フォーム */}
<div className="mb-4">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="TODOを入力してください"
className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button className="w-full mt-2 bg-blue-500 text-white p-3 rounded-lg hover:bg-blue-600 transition-colors">
</button>
</div>
{/* TODOリスト */}
<div className="space-y-2">
{todos.length === 0 ? (
<p className="text-gray-500 text-center py-8">TODOがありません</p>
) : (
todos.map((todo) => (
<div key={todo.id} className="flex items-center p-3 bg-gray-50 rounded-lg">
<input
type="checkbox"
checked={todo.completed}
className="mr-3 h-5 w-5"
/>
<span className={`flex-1 ${todo.completed ? 'line-through text-gray-500' : ''}`}>
{todo.text}
</span>
<button className="text-red-500 hover:text-red-700 ml-2"></button>
</div>
))
)}
</div>
</div>
</main>
);
}
```
**重要なポイント:**
- `'use client'` - クライアントサイドで実行するコンポーネントであることを宣言
- `useState<Todo[]>([])` - 空の配列で初期化、型はTodoの配列
- `onChange` - 入力が変更されるたびに実行される
- `map` - 配列の各要素をJSXに変換
- `key={todo.id}` - Reactが効率的に更新するために必要
**📚 このステップの参考文献:**
- [React公式 - useState](https://ja.react.dev/reference/react/useState)
- [TypeScript入門](https://typescriptbook.jp/overview/features)
### Phase 3: 機能実装 (4-5時間)
#### 学習目標
- CRUD操作の基本を理解する
- イベントハンドラーの実装方法を学ぶ
- 配列の操作(追加、更新、削除)を身につける
#### Step 3.1: TODO追加機能
**学習内容:**
- フォーム送信処理
- 新しいTODOの作成
- リストへの反映
**💡 詳細な実装と解説:**
```typescript
// TODO追加関数の実装
const addTodo = () => {
// 空の入力をチェック(バリデーション)
if (inputValue.trim() === '') {
alert('TODOを入力してください');
return;
}
// 新しいTODOオブジェクトを作成
const newTodo: Todo = {
id: Date.now(), // 簡易的なID生成本番ではuuidを推奨
text: inputValue,
completed: false
};
// 状態を更新(重要:配列をコピーして新しい配列を作る)
setTodos([...todos, newTodo]);
// 入力欄をクリア
setInputValue('');
};
// フォームで使う場合
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); // ページのリロードを防ぐ
addTodo();
};
// JSX部分
<form onSubmit={handleSubmit} className="mb-4">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="TODOを入力してEnterキー"
className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="submit"
className="w-full mt-2 bg-blue-500 text-white p-3 rounded-lg hover:bg-blue-600 transition-colors"
>
</button>
</form>
```
**なぜこのように書くのか:**
- `trim()` - 空白だけの入力を防ぐ
- `Date.now()` - ミリ秒単位のタイムスタンプでIDを生成簡易版
- `[...todos, newTodo]` - スプレッド構文で既存の配列をコピーして新しい要素を追加
- `e.preventDefault()` - formのデフォルト動作ページリロードを止める
**📚 このステップの参考文献:**
- [React公式 - フォームの処理](https://ja.react.dev/reference/react-dom/components/form)
- [JavaScript Array メソッド入門](https://zenn.dev/antez/articles/array-methods-cheat-sheet)
#### Step 3.2: TODO完了機能
**学習内容:**
- チェックボックスの実装
- 完了状態の切り替え
- 条件付きスタイリング
**💡 詳細な実装と解説:**
```typescript
// 完了状態を切り替える関数
const toggleTodo = (id: number) => {
// mapを使って新しい配列を作成イミュータビリティ
const updatedTodos = todos.map(todo => {
if (todo.id === id) {
// 該当するTODOを見つけたら、completedを反転
return { ...todo, completed: !todo.completed };
}
return todo; // それ以外はそのまま
});
setTodos(updatedTodos);
};
// TODOリストの表示部分
<div className="space-y-2">
{todos.map((todo) => (
<div key={todo.id} className="flex items-center p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
className="mr-3 h-5 w-5 cursor-pointer"
/>
<span
className={`flex-1 ${todo.completed ? 'line-through text-gray-500' : 'text-gray-800'}`}
>
{todo.text}
</span>
</div>
))}
</div>
```
**解説:**
- `map` - 配列の各要素を変換して新しい配列を作る
- `{ ...todo, completed: !todo.completed }` - スプレッド構文でtodoをコピーし、completedだけ上書き
- `checked={todo.completed}` - チェックボックスの状態をデータと同期
- `line-through` - 完了時に打ち消し線を表示Tailwind CSSのクラス
**よくあるミス:**
```typescript
// ❌ これは動かない!直接配列を変更している
const todo = todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed; // Reactが変更を検知できない
setTodos(todos); // 同じ配列をセットしても再レンダリングされない
}
```
**📚 このステップの参考文献:**
- [React公式 - 状態の更新](https://ja.react.dev/learn/updating-objects-in-state)
- [イミュータビリティの重要性](https://zenn.dev/okuoku/articles/react-immutability)
#### Step 3.3: TODO削除機能
**学習内容:**
- 削除ボタンの追加
- 配列からの要素削除
- 確認ダイアログ(オプション)
**💡 詳細な実装と解説:**
```typescript
// 削除関数
const deleteTodo = (id: number) => {
// 確認ダイアログを表示(オプション)
if (confirm('本当に削除しますか?')) {
// filterを使って該当ID以外の要素で新しい配列を作成
const filteredTodos = todos.filter(todo => todo.id !== id);
setTodos(filteredTodos);
}
};
// TODOリストに削除ボタンを追加
<div className="space-y-2">
{todos.map((todo) => (
<div key={todo.id} className="flex items-center p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
className="mr-3 h-5 w-5 cursor-pointer"
/>
<span
className={`flex-1 ${todo.completed ? 'line-through text-gray-500' : 'text-gray-800'}`}
>
{todo.text}
</span>
<button
onClick={() => deleteTodo(todo.id)}
className="text-red-500 hover:text-red-700 ml-2 px-2 py-1 rounded transition-colors"
>
</button>
</div>
))}
</div>
```
**解説:**
- `filter` - 条件に合う要素だけで新しい配列を作成
- `todo.id !== id` - 削除対象以外の要素を残す
- `confirm()` - ブラウザの確認ダイアログtrue/falseを返す
**カスタム確認ダイアログの例:**
```typescript
// 状態でモーダルを管理
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [deletingId, setDeletingId] = useState<number | null>(null);
const handleDeleteClick = (id: number) => {
setDeletingId(id);
setShowDeleteModal(true);
};
const confirmDelete = () => {
if (deletingId) {
setTodos(todos.filter(todo => todo.id !== deletingId));
}
setShowDeleteModal(false);
setDeletingId(null);
};
// モーダルコンポーネント
{showDeleteModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white p-6 rounded-lg shadow-lg">
<p className="mb-4 text-gray-800"></p>
<div className="flex gap-2">
<button
onClick={confirmDelete}
className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600 transition-colors"
>
</button>
<button
onClick={() => setShowDeleteModal(false)}
className="bg-gray-300 text-gray-800 px-4 py-2 rounded hover:bg-gray-400 transition-colors"
>
</button>
</div>
</div>
</div>
)}
```
**📚 このステップの参考文献:**
- [JavaScript Array.filter()](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/filter)
- [React Modal実装パターン](https://zenn.dev/yukikoma/articles/react-modal-patterns)
### Phase 4: 高度な機能 (3-4時間)
#### 学習目標
- 複雑な状態管理パターンを理解する
- インライン編集の実装方法を学ぶ
- データの永続化について理解する
#### Step 4.1: TODO編集機能
**学習内容:**
- インライン編集の実装
- 保存とキャンセル処理
- 複数状態の管理
**💡 詳細な実装と解説:**
```typescript
// 編集用の状態管理
const [editingId, setEditingId] = useState<number | null>(null);
const [editText, setEditText] = useState('');
// 編集開始関数
const startEdit = (todo: Todo) => {
setEditingId(todo.id);
setEditText(todo.text);
};
// 編集保存関数
const saveEdit = () => {
if (editText.trim() === '') {
alert('TODOが空です');
return;
}
const updatedTodos = todos.map(todo => {
if (todo.id === editingId) {
return { ...todo, text: editText };
}
return todo;
});
setTodos(updatedTodos);
setEditingId(null); // 編集モードを終了
setEditText('');
};
// 編集キャンセル関数
const cancelEdit = () => {
setEditingId(null);
setEditText('');
};
// TODOリストの表示部分
<div className="space-y-2">
{todos.map((todo) => (
<div key={todo.id} className="flex items-center p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
className="mr-3 h-5 w-5 cursor-pointer"
/>
{editingId === todo.id ? (
// 編集モード
<input
type="text"
value={editText}
onChange={(e) => setEditText(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') saveEdit();
if (e.key === 'Escape') cancelEdit();
}}
className="flex-1 border border-blue-500 p-2 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
autoFocus
/>
) : (
// 通常モード
<span
onClick={() => startEdit(todo)}
className={`flex-1 cursor-pointer p-2 rounded hover:bg-gray-200 transition-colors ${
todo.completed ? 'line-through text-gray-500' : 'text-gray-800'
}`}
>
{todo.text}
</span>
)}
{editingId === todo.id ? (
// 編集モードのボタン
<div className="flex gap-2 ml-2">
<button
onClick={saveEdit}
className="text-green-600 hover:text-green-800 px-2 py-1 rounded transition-colors"
>
</button>
<button
onClick={cancelEdit}
className="text-gray-600 hover:text-gray-800 px-2 py-1 rounded transition-colors"
>
</button>
</div>
) : (
// 通常モードのボタン
<div className="flex gap-2 ml-2">
<button
onClick={() => startEdit(todo)}
className="text-blue-500 hover:text-blue-700 px-2 py-1 rounded transition-colors"
>
</button>
<button
onClick={() => deleteTodo(todo.id)}
className="text-red-500 hover:text-red-700 px-2 py-1 rounded transition-colors"
>
</button>
</div>
)}
</div>
))}
</div>
```
**ポイント:**
- `editingId` - 現在編集中のTODOのIDを保持
- `onKeyDown` - キーボードイベントでEnter/Escを検知
- `autoFocus` - 編集開始時に自動的にフォーカス
- 三項演算子 `? :` - 条件によって表示を切り替え
**📚 このステップの参考文献:**
- [React公式 - 条件付きレンダリング](https://ja.react.dev/learn/conditional-rendering)
- [キーボードイベントの処理](https://zenn.dev/yoichi_dev/articles/react-keyboard-events)
#### Step 4.2: フィルタリング機能
**学習内容:**
- All/Active/Completedの切り替え
- 条件付きレンダリング
- 状態による表示制御
**💡 詳細な実装と解説:**
```typescript
// フィルタリング用の型定義
type FilterType = 'all' | 'active' | 'completed';
// フィルタリング状態の管理
const [filter, setFilter] = useState<FilterType>('all');
// フィルタリング関数
const getFilteredTodos = () => {
switch (filter) {
case 'active':
return todos.filter(todo => !todo.completed);
case 'completed':
return todos.filter(todo => todo.completed);
default:
return todos;
}
};
// 統計情報の計算
const activeCount = todos.filter(todo => !todo.completed).length;
const completedCount = todos.filter(todo => todo.completed).length;
// フィルタリングボタンの実装
<div className="flex justify-between items-center mb-4 p-4 bg-gray-100 rounded-lg">
<div className="flex gap-2">
<button
onClick={() => setFilter('all')}
className={`px-4 py-2 rounded transition-colors ${
filter === 'all'
? 'bg-blue-500 text-white'
: 'bg-white text-gray-700 hover:bg-gray-200'
}`}
>
({todos.length})
</button>
<button
onClick={() => setFilter('active')}
className={`px-4 py-2 rounded transition-colors ${
filter === 'active'
? 'bg-blue-500 text-white'
: 'bg-white text-gray-700 hover:bg-gray-200'
}`}
>
({activeCount})
</button>
<button
onClick={() => setFilter('completed')}
className={`px-4 py-2 rounded transition-colors ${
filter === 'completed'
? 'bg-blue-500 text-white'
: 'bg-white text-gray-700 hover:bg-gray-200'
}`}
>
({completedCount})
</button>
</div>
{/* 完了済みTODOをすべて削除 */}
{completedCount > 0 && (
<button
onClick={() => setTodos(todos.filter(todo => !todo.completed))}
className="text-red-500 hover:text-red-700 px-3 py-1 rounded transition-colors"
>
</button>
)}
</div>
// フィルタリング結果の表示
<div className="space-y-2">
{getFilteredTodos().length === 0 ? (
<p className="text-gray-500 text-center py-8">
{filter === 'active' && 'すべて完了しました!'}
{filter === 'completed' && '完了したTODOがありません'}
{filter === 'all' && 'TODOがありません'}
</p>
) : (
getFilteredTodos().map((todo) => (
// 通常のTODOアイテム表示
<div key={todo.id} className="flex items-center p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
{/* 省略通常のTODOアイテム */}
</div>
))
)}
</div>
```
**📚 このステップの参考文献:**
- [React State管理パターン](https://zenn.dev/uhyo/articles/react-state-management-2022)
- [条件付きレンダリングのベストプラクティス](https://zenn.dev/t_keshi/articles/react-conditional-rendering)
#### Step 4.3: データの永続化
**学習内容:**
- Local Storageの活用
- `useEffect`フックの理解
- データの保存と読み込み
**💡 詳細な実装と解説:**
```typescript
import { useState, useEffect } from 'react';
// Local Storage のキー
const STORAGE_KEY = 'todo-app-data';
export default function Home() {
const [todos, setTodos] = useState<Todo[]>([]);
const [inputValue, setInputValue] = useState('');
// 他の状態...
// アプリ起動時にデータを読み込み
useEffect(() => {
const savedTodos = localStorage.getItem(STORAGE_KEY);
if (savedTodos) {
try {
const parsedTodos = JSON.parse(savedTodos);
setTodos(parsedTodos);
} catch (error) {
console.error('データの読み込みに失敗しました:', error);
}
}
}, []); // 空の依存配列で初回のみ実行
// TODOが変更されるたびに保存
useEffect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
}, [todos]); // todosが変更されるたびに実行
// カスタムフックとして分離する場合
const useTodoStorage = (key: string) => {
const [todos, setTodos] = useState<Todo[]>([]);
// 初期データの読み込み
useEffect(() => {
const savedData = localStorage.getItem(key);
if (savedData) {
try {
setTodos(JSON.parse(savedData));
} catch (error) {
console.error('データの読み込みエラー:', error);
}
}
}, [key]);
// データの保存
useEffect(() => {
localStorage.setItem(key, JSON.stringify(todos));
}, [todos, key]);
return [todos, setTodos] as const;
};
// 使用例
// const [todos, setTodos] = useTodoStorage('my-todos');
// データのエクスポート機能
const exportData = () => {
const dataStr = JSON.stringify(todos, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = 'todos.json';
link.click();
URL.revokeObjectURL(url);
};
// データのインポート機能
const importData = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const importedTodos = JSON.parse(e.target?.result as string);
setTodos(importedTodos);
} catch (error) {
alert('ファイルの読み込みに失敗しました');
}
};
reader.readAsText(file);
}
};
return (
<main className="min-h-screen p-8 bg-gray-50">
<div className="max-w-md mx-auto bg-white rounded-lg shadow-md p-6">
{/* ヘッダー部分 */}
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold text-gray-800">TODOアプリ</h1>
<div className="flex gap-2">
<button
onClick={exportData}
className="text-blue-500 hover:text-blue-700 px-3 py-1 rounded transition-colors"
>
</button>
<label className="text-blue-500 hover:text-blue-700 px-3 py-1 rounded transition-colors cursor-pointer">
<input
type="file"
accept=".json"
onChange={importData}
className="hidden"
/>
</label>
</div>
</div>
{/* 残りのコンポーネント */}
</div>
</main>
);
}
```
**useEffectのポイント:**
- 第2引数の依存配列が重要
- `[]` - 初回のみ実行(コンポーネントマウント時)
- `[todos]` - todosが変更されるたびに実行
- cleanup関数が必要な場合は return で関数を返す
**📚 このステップの参考文献:**
- [React公式 - useEffect](https://ja.react.dev/reference/react/useEffect)
- [Local Storage活用術](https://zenn.dev/oubakiou/articles/localstorage-tips)
### Phase 5: 仕上げとベストプラクティス (2-3時間)
#### 学習目標
- コンポーネントの分割と再利用性を理解する
- パフォーマンス最適化の基本を学ぶ
- エラーハンドリングの実装方法を身につける
#### Step 5.1: コンポーネントの分割
**学習内容:**
- 再利用可能なコンポーネント化
- propsの型定義
- 関心の分離
**💡 詳細な実装と解説:**
```typescript
// src/app/components/TodoItem.tsx
interface TodoItemProps {
todo: Todo;
onToggle: (id: number) => void;
onDelete: (id: number) => void;
onEdit: (id: number, text: string) => void;
}
export function TodoItem({ todo, onToggle, onDelete, onEdit }: TodoItemProps) {
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState(todo.text);
const handleSave = () => {
if (editText.trim() === '') {
alert('TODOが空です');
return;
}
onEdit(todo.id, editText);
setIsEditing(false);
};
const handleCancel = () => {
setEditText(todo.text);
setIsEditing(false);
};
return (
<div className="flex items-center p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
className="mr-3 h-5 w-5 cursor-pointer"
/>
{isEditing ? (
<input
type="text"
value={editText}
onChange={(e) => setEditText(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleSave();
if (e.key === 'Escape') handleCancel();
}}
className="flex-1 border border-blue-500 p-2 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
autoFocus
/>
) : (
<span
onClick={() => setIsEditing(true)}
className={`flex-1 cursor-pointer p-2 rounded hover:bg-gray-200 transition-colors ${
todo.completed ? 'line-through text-gray-500' : 'text-gray-800'
}`}
>
{todo.text}
</span>
)}
{isEditing ? (
<div className="flex gap-2 ml-2">
<button onClick={handleSave} className="text-green-600 hover:text-green-800 px-2 py-1 rounded transition-colors">
</button>
<button onClick={handleCancel} className="text-gray-600 hover:text-gray-800 px-2 py-1 rounded transition-colors">
</button>
</div>
) : (
<div className="flex gap-2 ml-2">
<button
onClick={() => setIsEditing(true)}
className="text-blue-500 hover:text-blue-700 px-2 py-1 rounded transition-colors"
>
</button>
<button
onClick={() => onDelete(todo.id)}
className="text-red-500 hover:text-red-700 px-2 py-1 rounded transition-colors"
>
</button>
</div>
)}
</div>
);
}
// src/app/components/TodoList.tsx
interface TodoListProps {
todos: Todo[];
onToggle: (id: number) => void;
onDelete: (id: number) => void;
onEdit: (id: number, text: string) => void;
}
export function TodoList({ todos, onToggle, onDelete, onEdit }: TodoListProps) {
return (
<div className="space-y-2">
{todos.length === 0 ? (
<p className="text-gray-500 text-center py-8">TODOがありません</p>
) : (
todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onDelete={onDelete}
onEdit={onEdit}
/>
))
)}
</div>
);
}
// src/app/components/AddTodoForm.tsx
interface AddTodoFormProps {
onAdd: (text: string) => void;
}
export function AddTodoForm({ onAdd }: AddTodoFormProps) {
const [inputValue, setInputValue] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (inputValue.trim() === '') {
alert('TODOを入力してください');
return;
}
onAdd(inputValue);
setInputValue('');
};
return (
<form onSubmit={handleSubmit} className="mb-4">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="TODOを入力してください"
className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="submit"
className="w-full mt-2 bg-blue-500 text-white p-3 rounded-lg hover:bg-blue-600 transition-colors"
>
</button>
</form>
);
}
// メインコンポーネント
import { AddTodoForm } from './components/AddTodoForm';
import { TodoList } from './components/TodoList';
export default function Home() {
const [todos, setTodos] = useState<Todo[]>([]);
const addTodo = (text: string) => {
const newTodo: Todo = {
id: Date.now(),
text,
completed: false
};
setTodos([...todos, newTodo]);
};
const toggleTodo = (id: number) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id: number) => {
setTodos(todos.filter(todo => todo.id !== id));
};
const editTodo = (id: number, text: string) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, text } : todo
));
};
return (
<main className="min-h-screen p-8 bg-gray-50">
<div className="max-w-md mx-auto bg-white rounded-lg shadow-md p-6">
<h1 className="text-2xl font-bold text-gray-800 mb-6">TODOアプリ</h1>
<AddTodoForm onAdd={addTodo} />
<TodoList
todos={todos}
onToggle={toggleTodo}
onDelete={deleteTodo}
onEdit={editTodo}
/>
</div>
</main>
);
}
```
**コンポーネント分割のメリット:**
- 単一責任の原則に従う
- 再利用性が高まる
- テストが書きやすくなる
- コードが読みやすくなる
**📚 このステップの参考文献:**
- [React公式 - コンポーネントの分割](https://ja.react.dev/learn/thinking-in-react)
- [コンポーネント設計のベストプラクティス](https://zenn.dev/yoshiko/articles/99f8047555f700)
#### Step 5.2: エラーハンドリング
**学習内容:**
- 入力検証の強化
- エラーメッセージの表示
- エラーバウンダリーの実装
**💡 詳細な実装と解説:**
```typescript
// src/app/components/ErrorBoundary.tsx
'use client';
import { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false
};
public static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('エラーが発生しました:', error, errorInfo);
}
public render() {
if (this.state.hasError) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="bg-white p-8 rounded-lg shadow-md max-w-md">
<h2 className="text-2xl font-bold text-red-600 mb-4"></h2>
<p className="text-gray-700 mb-4">
</p>
<button
onClick={() => this.setState({ hasError: false, error: undefined })}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition-colors"
>
</button>
</div>
</div>
);
}
return this.props.children;
}
}
// カスタムフックでエラー処理
const useErrorHandler = () => {
const [error, setError] = useState<string | null>(null);
const handleError = (errorMessage: string) => {
setError(errorMessage);
setTimeout(() => setError(null), 5000); // 5秒後に自動で消す
};
const clearError = () => setError(null);
return { error, handleError, clearError };
};
// エラーメッセージコンポーネント
const ErrorMessage = ({ message, onClose }: { message: string; onClose: () => void }) => (
<div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded-lg flex justify-between items-center">
<span>{message}</span>
<button
onClick={onClose}
className="text-red-500 hover:text-red-700 font-bold"
>
×
</button>
</div>
);
// 入力検証の強化
const validateTodo = (text: string): string | null => {
if (text.trim() === '') {
return 'TODOを入力してください';
}
if (text.length > 100) {
return 'TODOは100文字以内で入力してください';
}
if (text.trim().length < 2) {
return 'TODOは2文字以上で入力してください';
}
return null;
};
// 使用例
export function AddTodoForm({ onAdd }: AddTodoFormProps) {
const [inputValue, setInputValue] = useState('');
const { error, handleError, clearError } = useErrorHandler();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
clearError();
const validationError = validateTodo(inputValue);
if (validationError) {
handleError(validationError);
return;
}
try {
onAdd(inputValue);
setInputValue('');
} catch (err) {
handleError('TODOの追加に失敗しました');
}
};
return (
<div className="mb-4">
{error && <ErrorMessage message={error} onClose={clearError} />}
<form onSubmit={handleSubmit}>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="TODOを入力してください"
className={`w-full p-3 border rounded-lg focus:outline-none focus:ring-2 ${
error ? 'border-red-500 focus:ring-red-500' : 'border-gray-300 focus:ring-blue-500'
}`}
/>
<button
type="submit"
className="w-full mt-2 bg-blue-500 text-white p-3 rounded-lg hover:bg-blue-600 transition-colors disabled:opacity-50"
disabled={inputValue.trim() === ''}
>
</button>
</form>
</div>
);
}
```
**📚 このステップの参考文献:**
- [React公式 - エラーバウンダリー](https://ja.react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)
- [React エラーハンドリング完全ガイド](https://zenn.dev/brachio_takumi/articles/react-error-handling)
#### Step 5.3: パフォーマンス最適化
**学習内容:**
- `useCallback`の基礎
- `useMemo`の基礎
- キー属性の重要性
**💡 詳細な実装と解説:**
```typescript
import { useState, useCallback, useMemo } from 'react';
export default function Home() {
const [todos, setTodos] = useState<Todo[]>([]);
const [filter, setFilter] = useState<FilterType>('all');
// useCallbackでコールバック関数をメモ化
const addTodo = useCallback((text: string) => {
const newTodo: Todo = {
id: Date.now(),
text,
completed: false
};
setTodos(prev => [...prev, newTodo]);
}, []);
const toggleTodo = useCallback((id: number) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
}, []);
const deleteTodo = useCallback((id: number) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);
const editTodo = useCallback((id: number, text: string) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, text } : todo
));
}, []);
// useMemoで計算結果をメモ化
const filteredTodos = useMemo(() => {
switch (filter) {
case 'active':
return todos.filter(todo => !todo.completed);
case 'completed':
return todos.filter(todo => todo.completed);
default:
return todos;
}
}, [todos, filter]);
const stats = useMemo(() => ({
total: todos.length,
active: todos.filter(todo => !todo.completed).length,
completed: todos.filter(todo => todo.completed).length
}), [todos]);
return (
<main className="min-h-screen p-8 bg-gray-50">
<div className="max-w-md mx-auto bg-white rounded-lg shadow-md p-6">
<h1 className="text-2xl font-bold text-gray-800 mb-6">
TODOアプリ ({stats.total})
</h1>
<AddTodoForm onAdd={addTodo} />
<FilterButtons
filter={filter}
onFilterChange={setFilter}
stats={stats}
/>
<TodoList
todos={filteredTodos}
onToggle={toggleTodo}
onDelete={deleteTodo}
onEdit={editTodo}
/>
</div>
</main>
);
}
// React.memoでコンポーネントをメモ化
const FilterButtons = React.memo(({ filter, onFilterChange, stats }: {
filter: FilterType;
onFilterChange: (filter: FilterType) => void;
stats: { total: number; active: number; completed: number };
}) => {
return (
<div className="flex gap-2 mb-4">
<button
onClick={() => onFilterChange('all')}
className={`px-4 py-2 rounded transition-colors ${
filter === 'all'
? 'bg-blue-500 text-white'
: 'bg-gray-200 hover:bg-gray-300'
}`}
>
({stats.total})
</button>
<button
onClick={() => onFilterChange('active')}
className={`px-4 py-2 rounded transition-colors ${
filter === 'active'
? 'bg-blue-500 text-white'
: 'bg-gray-200 hover:bg-gray-300'
}`}
>
({stats.active})
</button>
<button
onClick={() => onFilterChange('completed')}
className={`px-4 py-2 rounded transition-colors ${
filter === 'completed'
? 'bg-blue-500 text-white'
: 'bg-gray-200 hover:bg-gray-300'
}`}
>
({stats.completed})
</button>
</div>
);
});
```
**パフォーマンス最適化のポイント:**
- `useCallback` - 関数の再生成を防ぐ
- `useMemo` - 計算結果をキャッシュ
- `React.memo` - 不要な再レンダリングを防ぐ
- 適切なkey属性の使用
**📚 このステップの参考文献:**
- [React公式 - パフォーマンス最適化](https://ja.react.dev/learn/render-and-commit)
- [React Hooks パフォーマンスガイド](https://zenn.dev/uhyo/articles/react-hooks-performance)
## つまずきやすいポイントと対策
### 1. TypeScriptのエラー
**よくあるエラーと解決策:**
```typescript
// エラー例1: Parameter 'e' implicitly has an 'any' type
// ❌ エラーが出るコード
const handleChange = (e) => {
setInputValue(e.target.value);
};
// ✅ 解決策1: 型を明示的に指定
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
};
// ✅ 解決策2: インラインで書く(型推論が効く)
<input onChange={(e) => setInputValue(e.target.value)} />
// エラー例2: Object is possibly 'undefined'
// ❌ エラーが出るコード
const todo = todos.find(t => t.id === id);
console.log(todo.text); // todoがundefinedの可能性
// ✅ 解決策: オプショナルチェイニング
console.log(todo?.text);
// または条件分岐
if (todo) {
console.log(todo.text);
}
```
**VSCodeの便利機能:**
- 変数にカーソルを合わせると型が表示される
- Ctrl+Space で補完候補を表示
- エラーの上にカーソルを置くと「Quick Fix」が提案される
### 2. 状態更新のイミュータビリティ
**よくある間違いと正しい書き方:**
```typescript
// ❌ NGパターン
todos.push(newTodo); // 配列を直接変更
todos[0].completed = true; // オブジェクトを直接変更
// ✅ OKパターン
setTodos([...todos, newTodo]); // 新しい配列を作成
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: true } : todo
));
```
### 3. useEffectの依存配列
**よくある間違い:**
```typescript
// ❌ 依存配列を忘れる(無限ループの原因)
useEffect(() => {
setTodos(todos.filter(todo => todo.completed));
}); // 依存配列がない
// ✅ 正しい書き方
useEffect(() => {
// 何らかの処理
}, [todos]); // 依存配列を指定
```
### 4. イベントハンドラーの型
**正しい型定義:**
```typescript
// フォーム送信
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// 処理
};
// 入力変更
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
};
// ボタンクリック
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
// 処理
};
```
## 評価基準
### 基本機能 (必須)
- [ ] TODOの追加ができる
- [ ] TODOの完了/未完了の切り替えができる
- [ ] TODOの削除ができる
- [ ] 適切な型定義がされている
- [ ] エラーハンドリングが実装されている
### 追加機能 (推奨)
- [ ] TODOの編集ができる
- [ ] フィルタリング機能がある
- [ ] データが永続化される
- [ ] レスポンシブデザインに対応
- [ ] コンポーネントが適切に分割されている
### コード品質
- [ ] TypeScriptの型安全性が確保されている
- [ ] コードが読みやすく保守性が高い
- [ ] 適切なエラーハンドリングが実装されている
- [ ] パフォーマンスが考慮されている
## 発展課題
1. **ドラッグ&ドロップ**での並び替え
- react-beautiful-dndライブラリの使用
- カスタムフックでの実装
2. **カテゴリー機能**の追加
- TODOのカテゴリ分類
- カテゴリー別フィルタリング
3. **期限設定**機能
- 期限日の設定
- 期限切れの警告表示
4. **Next.js API Routes**を使ったバックエンド実装
- データベースとの連携
- RESTful APIの実装
5. **認証機能**の追加NextAuth.js
- ユーザー登録・ログイン
- ユーザー別TODOの管理
6. **PWAProgressive Web App対応**
- オフライン対応
- プッシュ通知
7. **テストの実装**
- Jest + React Testing Library
- E2EテストPlaywright
## 推奨リソース
### 📚 公式ドキュメント
- [Next.js公式ドキュメント](https://nextjs.org/docs)
- [React公式ドキュメント](https://ja.react.dev/)
- [TypeScript Handbook](https://www.typescriptlang.org/docs/)
- [Tailwind CSS公式ドキュメント](https://tailwindcss.com/docs)
### 🎥 日本語の動画教材
- [【2024年最新】React完全入門ガイドHooks、Next.js、TypeScriptまで](https://www.youtube.com/watch?v=TGiCr7S2E7E)
- [Next.js入門 - App Routerを基礎から学ぶ](https://www.youtube.com/watch?v=BVq8n8tVNJg)
- [TypeScript入門 完全版](https://www.youtube.com/watch?v=F9vzRz6jyRk)
- [React Hooks完全入門](https://www.youtube.com/watch?v=xkxZWFVU2MA)
### 📖 日本語の参考記事・書籍
- [サバイバルTypeScript](https://typescriptbook.jp/) - TypeScriptの実践的な解説
- [React入門 TODOアプリ開発](https://zenn.dev/topics/react-todo) - Zennの関連記事集
- [Next.js App Router入門](https://zenn.dev/hayato94087/articles/e9712c4ab7a8f1) - 詳細な解説記事
- [モダンJavaScript入門](https://zenn.dev/antez/books/modern-javascript-1) - JavaScript基礎の復習
- 書籍:「React実践入門」技術評論社
- 書籍:「TypeScript実践プログラミング」マイナビ出版
### 🛠️ 実践的な学習リソース
- [React Tutorial: Tic-Tac-Toe日本語版](https://ja.react.dev/learn/tutorial-tic-tac-toe) - 公式チュートリアル
- [TypeScript Playground](https://www.typescriptlang.org/play) - ブラウザで試せる
- [StackBlitz](https://stackblitz.com/) - オンラインでNext.jsを試せる
- [CodeSandbox](https://codesandbox.io/) - オンライン開発環境
### 🔧 開発ツール
- [React Developer Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi) - Chrome拡張
- [Redux DevTools](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd) - 状態管理デバッグ
- [VS Code拡張機能集](https://zenn.dev/antez/articles/vscode-extensions-for-react)
## サポート方法
1. **ペアプログラミング**: 詰まったらすぐに質問
2. **コードレビュー**: 各Phaseごとにレビュー
3. **デバッグセッション**: エラーの読み方を一緒に学習
4. **ベストプラクティス共有**: より良い書き方を随時提案
## 学習の進め方アドバイス
### 初心者がつまずきやすいポイント
1. **エラーメッセージを恐れない**
- エラーは学習のチャンス
- エラーメッセージをしっかり読んでコピー&ペーストでGoogle検索
2. **小さく始める**
- いきなり全部を理解しようとしない
- まず動くものを作ってから理解を深める
3. **コードを写経する**
- 最初はサンプルコードをそのまま写す
- 動いたら少しずつ変更して実験
4. **質問の仕方を覚える**
- 何をしようとしているのか
- 何が起きているのか(エラーメッセージ)
- 何を期待しているのか
### 各Phaseの確認ポイント
#### Phase 1 終了時
- [ ] `npm run dev`でアプリが起動する
- [ ] ブラウザに「Hello World」が表示される
- [ ] コンポーネントを自分で作成できる
#### Phase 2 終了時
- [ ] TODOの入力フォームが表示される
- [ ] TODOリストが表示されるまだ空でOK
- [ ] スタイルが適用されている
#### Phase 3 終了時
- [ ] TODOを追加できる
- [ ] TODOを完了/未完了に切り替えられる
- [ ] TODOを削除できる
#### Phase 4 終了時
- [ ] TODOを編集できる
- [ ] フィルタリング機能が動作する
- [ ] データが永続化される
#### Phase 5 終了時
- [ ] コンポーネントが適切に分割されている
- [ ] エラーハンドリングが実装されている
- [ ] パフォーマンスが最適化されている
## FAQよくある質問
### Q: `'use client'`を忘れてエラーが出ました
**A:** useStateなどのReact Hooksを使う時は、ファイルの先頭に`'use client'`が必要です。Next.js 13+ App Routerでは、デフォルトでServer Componentとして動作するためです。
### Q: 「Cannot find module」エラーが出ます
**A:** importのパスが間違っている可能性があります。
- `./`で始まる相対パス
- `@/`で始まるエイリアスtsconfig.jsonで設定
- ファイル名の拡張子を確認
### Q: スタイルが適用されません
**A:**
- `className`を使っているか確認(`class`ではない)
- Tailwind CSSのクラス名が正しいか確認
- `globals.css`がimportされているか確認
### Q: TODOが保存されずリロードすると消えます
**A:** Phase 4のLocal Storage実装まではこれが正常です。メモリ上にしかデータが保存されていません。
### Q: TypeScriptの型エラーが難しいです
**A:**
- 最初は`any`型を使ってもOK
- VSCodeの型情報を活用
- 公式ドキュメントで型を確認
- 段階的に適切な型に置き換え
### Q: mapやfilterがよく分かりません
**A:** これらはJavaScriptの配列メソッドです。
- [Array.prototype.map()](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/map)
- [Array.prototype.filter()](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/filter)
### Q: デバッグの方法が分かりません
**A:**
- `console.log()`でデータを確認
- React Developer Toolsを使用
- ブラウザのDeveloper Toolsを活用
- エラーメッセージをしっかり読む
### Q: Next.jsとReactの違いがよく分かりません
**A:**
- ReactUIライブラリ
- Next.jsReactベースのフレームワーク
- Next.jsはReactに以下を追加
- ルーティング
- SSR/SSG
- API Routes
- 最適化機能
---
このカリキュラムは柔軟に調整可能です。学習者のペースに合わせて進めてください。
**🎯 最終目標実用的なTODOアプリを完成させ、React/Next.js/TypeScriptの基本をマスターする**