forked from semi-23e/nextjs-todo-tutorial
20 KiB
20 KiB
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の使い方)
💡 詳細な実装手順:
- 新しいコンポーネントファイルを作成
// 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の予約語と競合を避けるため)
- コンポーネントを使ってみる
// 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アイテムの型定義
type Todo = {
id: number;
text: string;
completed: boolean;
};
Phase 3: 機能実装 (4-5時間)
Step 3.1: TODO追加機能
- フォーム送信処理
- 新しいTODOの作成
- リストへの反映
💡 詳細な実装と解説:
// 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完了機能
- チェックボックスの実装
- 完了状態の切り替え
- 条件付きスタイリング
💡 詳細な実装と解説:
// 完了状態を切り替える関数
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のクラス)
よくあるミス:
// ❌ これは動かない!直接配列を変更している
const todo = todos.find(t => t.id === id);
todo.completed = !todo.completed; // Reactが変更を検知できない
setTodos(todos); // 同じ配列をセットしても再レンダリングされない
Step 3.3: TODO削除機能
- 削除ボタンの追加
- 配列からの要素削除
- 確認ダイアログ(オプション)
💡 詳細な実装と解説:
// 削除関数
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を返す)
カスタム確認ダイアログの例:
// 状態でモーダルを管理
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編集機能
- インライン編集の実装
- 保存とキャンセル処理
💡 詳細な実装と解説:
// 編集用の状態管理
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 実装例
// 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 実装例
// TODOの状態管理
const [todos, setTodos] = useState<Todo[]>([]);
💡 完全な実装例とコード解説:
// 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エラーと解決策:
// エラー例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を使用
// NG
todos.push(newTodo);
// OK
setTodos([...todos, newTodo]);
3. イベントハンドラーの型
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// 処理
};
評価基準
基本機能 (必須)
- TODOの追加ができる
- TODOの完了/未完了の切り替えができる
- TODOの削除ができる
- 適切な型定義がされている
追加機能 (推奨)
- TODOの編集ができる
- フィルタリング機能がある
- データが永続化される
- レスポンシブデザイン
コード品質
- コンポーネントが適切に分割されている
- エラーハンドリングがされている
- コードが読みやすい
発展課題
- ドラッグ&ドロップでの並び替え
- カテゴリー機能の追加
- 期限設定機能
- Next.js API Routesを使ったバックエンド実装
- 認証機能の追加(NextAuth.js)
推奨リソース
📚 公式ドキュメント
🎥 日本語の動画教材
📖 日本語の参考記事・書籍
- サバイバルTypeScript - TypeScriptの実践的な解説
- React入門 TODOアプリ開発 - Zennの関連記事集
- Next.js App Router入門 - 詳細な解説記事
- 書籍:「React実践入門」(技術評論社)
- 書籍:「TypeScript実践プログラミング」(マイナビ出版)
🛠️ 実践的な学習リソース
- React Tutorial: Tic-Tac-Toe(日本語版) - 公式チュートリアル
- TypeScript Playground - ブラウザで試せる
- StackBlitz - オンラインでNext.jsを試せる
サポート方法
- ペアプログラミング: 詰まったらすぐに質問
- コードレビュー: 各Phaseごとにレビュー
- デバッグセッション: エラーの読み方を一緒に学習
- ベストプラクティス共有: より良い書き方を随時提案
このカリキュラムは柔軟に調整可能です。学習者のペースに合わせて進めてください。
👨🏫 学習の進め方アドバイス
初心者がつまずきやすいポイント
-
エラーメッセージを恐れない
- エラーは学習のチャンス
- エラーメッセージをしっかり読んでコピー&ペーストでGoogle検索
-
小さく始める
- いきなり全部を理解しようとしない
- まず動くものを作ってから理解を深める
-
コードを写経する
- 最初はサンプルコードをそのまま写す
- 動いたら少しずつ変更して実験
各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の日本語ドキュメントで詳しく解説されています。