Files
nextjs-todo-tutorial/todo-app-curriculum.md
2025-07-08 16:11:22 +09:00

722 lines
20 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の基本文法変数、関数、配列、オブジェクト
- コマンドラインの基本操作
## 学習ステップ
### Phase 1: 基礎理解 (2-3時間)
#### Step 1.1: プロジェクト構造の理解
- `src/app/`ディレクトリの役割
- `page.tsx``layout.tsx`の違い
- TypeScriptファイル.ts/.tsxの意味
#### Step 1.2: 最初のコンポーネント作成
- Hello Worldコンポーネントの作成
- JSXの基本文法
- 型定義の初歩stringやnumberの使い方
**💡 詳細な実装手順:**
1. **新しいコンポーネントファイルを作成**
```typescript
// src/app/components/HelloWorld.tsx
// Reactコンポーネントの基本形
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し忘れていないか確認
### Phase 2: TODOアプリの骨組み作成 (3-4時間)
#### Step 2.1: UIコンポーネントの作成
- TODOリストの表示部分
- TODO入力フォーム
- Tailwind CSSによるスタイリング
#### Step 2.2: 状態管理の導入
- `useState`フックの使い方
- TODOアイテムの型定義
```typescript
type Todo = {
id: number;
text: string;
completed: boolean;
};
```
### Phase 3: 機能実装 (4-5時間)
#### 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="flex gap-2 mb-4">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
className="flex-1 border p-2 rounded"
placeholder="TODOを入力してEnterキー"
/>
<button
type="submit"
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
>
</button>
</form>
```
**なぜこのように書くのか:**
- `trim()` - 空白だけの入力を防ぐ
- `Date.now()` - ミリ秒単位のタイムスタンプでIDを生成簡易版
- `[...todos, newTodo]` - スプレッド構文で既存の配列をコピーして新しい要素を追加
- `e.preventDefault()` - formのデフォルト動作ページリロードを止める
#### 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リストの表示部分
<ul className="space-y-2">
{todos.map((todo) => (
<li
key={todo.id}
className="flex items-center gap-3 p-3 border rounded hover:bg-gray-50"
>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
className="w-5 h-5 cursor-pointer"
/>
<span
className={`flex-1 ${todo.completed ? 'line-through text-gray-500' : ''}`}
>
{todo.text}
</span>
</li>
))}
</ul>
```
**解説:**
- `map` - 配列の各要素を変換して新しい配列を作る
- `{ ...todo, completed: !todo.completed }` - スプレッド構文でtodoをコピーし、completedだけ上書き
- `checked={todo.completed}` - チェックボックスの状態をデータと同期
- `line-through` - 完了時に打ち消し線を表示Tailwind CSSのクラス
**よくあるミス:**
```typescript
// ❌ これは動かない!直接配列を変更している
const todo = todos.find(t => t.id === id);
todo.completed = !todo.completed; // Reactが変更を検知できない
setTodos(todos); // 同じ配列をセットしても再レンダリングされない
```
#### Step 3.3: TODO削除機能
- 削除ボタンの追加
- 配列からの要素削除
- 確認ダイアログ(オプション)
**💡 詳細な実装と解説:**
```typescript
// 削除関数
const deleteTodo = (id: number) => {
// 確認ダイアログを表示(オプション)
if (confirm('本当に削除しますか?')) {
// filterを使って該当ID以外の要素で新しい配列を作成
const filteredTodos = todos.filter(todo => todo.id !== id);
setTodos(filteredTodos);
}
};
// TODOリストに削除ボタンを追加
<li className="flex items-center gap-3 p-3 border rounded hover:bg-gray-50">
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
className="w-5 h-5 cursor-pointer"
/>
<span className={`flex-1 ${todo.completed ? 'line-through text-gray-500' : ''}`}>
{todo.text}
</span>
<button
onClick={() => deleteTodo(todo.id)}
className="text-red-500 hover:text-red-700 px-2 py-1 text-sm"
>
</button>
</li>
```
**解説:**
- `filter` - 条件に合う要素だけで新しい配列を作成
- `todo.id !== id` - 削除対象以外の要素を残す
- `confirm()` - ブラウザの確認ダイアログtrue/falseを返す
**カスタム確認ダイアログの例:**
```typescript
// 状態でモーダルを管理
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [deletingId, setDeletingId] = useState<number | null>(null);
// 削除モーダルコンポーネント
{showDeleteModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
<div className="bg-white p-6 rounded-lg">
<p className="mb-4"></p>
<div className="flex gap-2">
<button
onClick={() => {
if (deletingId) deleteTodo(deletingId);
setShowDeleteModal(false);
}}
className="bg-red-500 text-white px-4 py-2 rounded"
>
</button>
<button
onClick={() => setShowDeleteModal(false)}
className="bg-gray-300 px-4 py-2 rounded"
>
</button>
</div>
</div>
</div>
)}
```
### 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); // 編集モードを終了
};
// 編集キャンセル関数
const cancelEdit = () => {
setEditingId(null);
setEditText('');
};
// TODOリストの表示部分
{todos.map((todo) => (
<li key={todo.id} className="flex items-center gap-3 p-3 border rounded">
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
className="w-5 h-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 p-1 rounded"
autoFocus
/>
) : (
// 通常モード
<span
onClick={() => startEdit(todo)}
className={`flex-1 cursor-pointer ${todo.completed ? 'line-through text-gray-500' : ''}`}
>
{todo.text}
</span>
)}
{editingId === todo.id ? (
// 編集モードのボタン
<div className="flex gap-1">
<button onClick={saveEdit} className="text-green-600 text-sm">
</button>
<button onClick={cancelEdit} className="text-gray-600 text-sm">
</button>
</div>
) : (
// 通常モードのボタン
<button
onClick={() => deleteTodo(todo.id)}
className="text-red-500 hover:text-red-700 text-sm"
>
</button>
)}
</li>
))}
```
**ポイント:**
- `editingId` - 現在編集中のTODOのIDを保持
- `onKeyDown` - キーボードイベントでEnter/Escを検知
- `autoFocus` - 編集開始時に自動的にフォーカス
- 三項演算子 `? :` - 条件によって表示を切り替え
#### Step 4.2: フィルタリング機能
- All/Active/Completedの切り替え
- 条件付きレンダリング
#### Step 4.3: データの永続化
- Local Storageの活用
- `useEffect`フックの理解
### Phase 5: 仕上げとベストプラクティス (2-3時間)
#### Step 5.1: コンポーネントの分割
- 再利用可能なコンポーネント化
- propsの型定義
#### Step 5.2: エラーハンドリング
- 入力検証
- エラーメッセージの表示
#### Step 5.3: パフォーマンス最適化
- `useCallback`の基礎
- キー属性の重要性
## 各ステップの実装詳細
### Step 1.1 実装例
```typescript
// src/app/page.tsx
export default function Home() {
return (
<main className="min-h-screen p-8">
<h1 className="text-3xl font-bold">TODOアプリ</h1>
</main>
);
}
```
### Step 2.2 実装例
```typescript
// TODOの状態管理
const [todos, setTodos] = useState<Todo[]>([]);
```
**💡 完全な実装例とコード解説:**
```typescript
// src/app/page.tsx
'use client'; // ← 重要!状態管理を使う時は必須
import { useState } from 'react';
// TypeScriptの型定義
// interfaceまたはtypeで定義可能
type Todo = {
id: number; // 一意の識別子
text: string; // TODOの内容
completed: boolean; // 完了状態
};
export default function Home() {
// useState フックの使い方
// const [状態変数, 状態を更新する関数] = useState<型>(初期値);
const [todos, setTodos] = useState<Todo[]>([]);
// 入力フォームの状態も管理
const [inputValue, setInputValue] = useState<string>('');
return (
<main className="min-h-screen p-8">
<h1 className="text-3xl font-bold mb-8">TODOアプリ</h1>
{/* 入力フォーム */}
<div className="mb-4">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
className="border p-2 rounded"
placeholder="TODOを入力"
/>
</div>
{/* TODOリスト表示 */}
<ul className="space-y-2">
{todos.map((todo) => (
<li key={todo.id} className="p-2 border rounded">
{todo.text}
</li>
))}
</ul>
</main>
);
}
```
**解説:**
- `'use client'` - クライアントサイドで実行するコンポーネントであることを宣言
- `useState<Todo[]>([])` - 空の配列で初期化、型はTodoの配列
- `onChange` - 入力が変更されるたびに実行される
- `map` - 配列の各要素をJSXに変換
- `key={todo.id}` - Reactが効率的に更新するために必要
## つまずきやすいポイントと対策
### 1. TypeScriptのエラー
- 型定義が分からない → 最初はanyを使い、後から具体的な型に修正
- 型推論に頼る → VSCodeの補完機能を活用
**💡 よくある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. 状態更新のイミュータビリティ
- 配列の直接変更はNG → スプレッド構文やfilter/mapを使用
```typescript
// NG
todos.push(newTodo);
// OK
setTodos([...todos, newTodo]);
```
### 3. イベントハンドラーの型
```typescript
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// 処理
};
```
## 評価基準
### 基本機能 (必須)
- [ ] TODOの追加ができる
- [ ] TODOの完了/未完了の切り替えができる
- [ ] TODOの削除ができる
- [ ] 適切な型定義がされている
### 追加機能 (推奨)
- [ ] TODOの編集ができる
- [ ] フィルタリング機能がある
- [ ] データが永続化される
- [ ] レスポンシブデザイン
### コード品質
- [ ] コンポーネントが適切に分割されている
- [ ] エラーハンドリングがされている
- [ ] コードが読みやすい
## 発展課題
1. **ドラッグ&ドロップ**での並び替え
2. **カテゴリー機能**の追加
3. **期限設定**機能
4. **Next.js API Routes**を使ったバックエンド実装
5. **認証機能**の追加NextAuth.js
## 推奨リソース
### 📚 公式ドキュメント
- [Next.js公式ドキュメント](https://nextjs.org/docs)
- [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html)
- [React公式チュートリアル](https://react.dev/learn)
- [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)
### 📖 日本語の参考記事・書籍
- [サバイバルTypeScript](https://typescriptbook.jp/) - TypeScriptの実践的な解説
- [React入門 TODOアプリ開発](https://zenn.dev/topics/react-todo) - Zennの関連記事集
- [Next.js App Router入門](https://zenn.dev/hayato94087/articles/e9712c4ab7a8f1) - 詳細な解説記事
- 書籍:「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を試せる
## サポート方法
1. **ペアプログラミング**: 詰まったらすぐに質問
2. **コードレビュー**: 各Phaseごとにレビュー
3. **デバッグセッション**: エラーの読み方を一緒に学習
4. **ベストプラクティス共有**: より良い書き方を随時提案
---
このカリキュラムは柔軟に調整可能です。学習者のペースに合わせて進めてください。
## 👨‍🏫 学習の進め方アドバイス
### 初心者がつまずきやすいポイント
1. **エラーメッセージを恐れない**
- エラーは学習のチャンス
- エラーメッセージをしっかり読んでコピー&ペーストでGoogle検索
2. **小さく始める**
- いきなり全部を理解しようとしない
- まず動くものを作ってから理解を深める
3. **コードを写経する**
- 最初はサンプルコードをそのまま写す
- 動いたら少しずつ変更して実験
### 各Phaseの確認ポイント
#### Phase 1 終了時
- [ ] `npm run dev`でアプリが起動する
- [ ] ブラウザに「Hello World」が表示される
- [ ] コンポーネントを自分で作成できる
#### Phase 2 終了時
- [ ] TODOの入力フォームが表示される
- [ ] TODOリストが表示されるまだ空でOK
- [ ] スタイルが適用されている
#### Phase 3 終了時
- [ ] TODOを追加できる
- [ ] TODOを完了/未完了に切り替えられる
- [ ] TODOを削除できる
## 🔥 FAQよくある質問
### Q: `'use client'`を忘れてエラーが出ました
**A:** useStateなどのフックを使う時は、ファイルの先頭に`'use client'`が必要です。
### Q: 「Cannot find module」エラーが出ます
**A:** importのパスが間違っている可能性があります。`./`で始まる相対パスか、`@/`で始まるエイリアスを確認してください。
### Q: スタイルが適用されません
**A:** classNameを使っているか確認してくださいclassではなく。また、Tailwind CSSのクラス名が正しいか公式ドキュメントで確認しましょう。
### Q: TODOが保存されずリロードすると消えます
**A:** Phase 4のLocal Storage実装まではこれが正常です。メモリ上にしかデータが保存されていません。
### Q: TypeScriptの型エラーが難しいです
**A:** 最初は`any`型を使ってもOKです。動くようになったら徐々に適切な型に置き換えていきましょう。
### Q: mapやfilterがよく分かりません
**A:** これらはJavaScriptの配列メソッドです。MDNの日本語ドキュメントで詳しく解説されています。
- [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)