diff --git a/todo-app-curriculum.md b/todo-app-curriculum.md index 380833a..e8cb047 100644 --- a/todo-app-curriculum.md +++ b/todo-app-curriculum.md @@ -18,29 +18,92 @@ - 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 -// Reactコンポーネントの基本形 export default function HelloWorld() { return (
@@ -51,7 +114,7 @@ export default function HelloWorld() { } ``` -**解説:** +**解説:** - `export default function` - このコンポーネントを他のファイルから使えるようにする - JSXは見た目はHTMLに似ているが、実はJavaScriptの拡張構文 @@ -73,41 +136,183 @@ export default function Home() { } ``` -**エラーが出た場合:** +**よくあるエラーと対処法:** - `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 ( +
+
+

TODOアプリ

+ + {/* 入力フォーム */} +
+ + +
+ + {/* TODOリスト */} +
+
+ + サンプルTODO + +
+
+
+
+ ); +} +``` + +**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; - completed: boolean; + id: number; // 一意の識別子 + text: string; // TODOの内容 + completed: boolean; // 完了状態 }; + +export default function Home() { + // useState フックの使い方 + const [todos, setTodos] = useState([]); + const [inputValue, setInputValue] = useState(''); + + return ( +
+
+

TODOアプリ

+ + {/* 入力フォーム */} +
+ 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" + /> + +
+ + {/* TODOリスト */} +
+ {todos.length === 0 ? ( +

TODOがありません

+ ) : ( + todos.map((todo) => ( +
+ + + {todo.text} + + +
+ )) + )} +
+
+
+ ); +} ``` +**重要なポイント:** + +- `'use client'` - クライアントサイドで実行するコンポーネントであることを宣言 +- `useState([])` - 空の配列で初期化、型は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追加関数の実装 @@ -139,37 +344,44 @@ const handleSubmit = (e: React.FormEvent) => { }; // JSX部分 -
+ setInputValue(e.target.value)} - className="flex-1 border p-2 rounded" placeholder="TODOを入力してEnterキー" + className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" />
``` -**なぜこのように書くのか:** +**なぜこのように書くのか:** - `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 // 完了状態を切り替える関数 @@ -187,51 +399,57 @@ const toggleTodo = (id: number) => { }; // TODOリストの表示部分 -
    +
    {todos.map((todo) => ( -
  • +
    toggleTodo(todo.id)} - className="w-5 h-5 cursor-pointer" + className="mr-3 h-5 w-5 cursor-pointer" /> {todo.text} -
  • +
    ))} -
+
``` -**解説:** +**解説:** - `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); // 同じ配列をセットしても再レンダリングされない +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 // 削除関数 @@ -245,56 +463,72 @@ const deleteTodo = (id: number) => { }; // TODOリストに削除ボタンを追加 -
  • - toggleTodo(todo.id)} - className="w-5 h-5 cursor-pointer" - /> - - {todo.text} - - -
  • +
    + {todos.map((todo) => ( +
    + toggleTodo(todo.id)} + className="mr-3 h-5 w-5 cursor-pointer" + /> + + {todo.text} + + +
    + ))} +
    ``` -**解説:** +**解説:** - `filter` - 条件に合う要素だけで新しい配列を作成 - `todo.id !== id` - 削除対象以外の要素を残す - `confirm()` - ブラウザの確認ダイアログ(true/falseを返す) -**カスタム確認ダイアログの例:** +**カスタム確認ダイアログの例:** ```typescript // 状態でモーダルを管理 const [showDeleteModal, setShowDeleteModal] = useState(false); const [deletingId, setDeletingId] = useState(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 && ( -
    -
    -

    本当に削除しますか?

    +
    +
    +

    本当に削除しますか?

    @@ -304,14 +538,28 @@ const [deletingId, setDeletingId] = useState(null); )} ``` +**📚 このステップの参考文献:** + +- [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 // 編集用の状態管理 @@ -340,6 +588,7 @@ const saveEdit = () => { setTodos(updatedTodos); setEditingId(null); // 編集モードを終了 + setEditText(''); }; // 編集キャンセル関数 @@ -349,185 +598,869 @@ const cancelEdit = () => { }; // TODOリストの表示部分 -{todos.map((todo) => ( -
  • - toggleTodo(todo.id)} - className="w-5 h-5 cursor-pointer" - /> - - {editingId === todo.id ? ( - // 編集モード +
    + {todos.map((todo) => ( +
    setEditText(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') saveEdit(); - if (e.key === 'Escape') cancelEdit(); - }} - className="flex-1 border p-1 rounded" - autoFocus + type="checkbox" + checked={todo.completed} + onChange={() => toggleTodo(todo.id)} + className="mr-3 h-5 w-5 cursor-pointer" /> - ) : ( - // 通常モード - startEdit(todo)} - className={`flex-1 cursor-pointer ${todo.completed ? 'line-through text-gray-500' : ''}`} - > - {todo.text} - - )} - - {editingId === todo.id ? ( - // 編集モードのボタン -
    - - -
    - ) : ( - // 通常モードのボタン - - )} -
  • -))} + + {editingId === todo.id ? ( + // 編集モード + 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 + /> + ) : ( + // 通常モード + 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} + + )} + + {editingId === todo.id ? ( + // 編集モードのボタン +
    + + +
    + ) : ( + // 通常モードのボタン +
    + + +
    + )} +
    + ))} +
    ``` -**ポイント:** +**ポイント:** - `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('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; + +// フィルタリングボタンの実装 +
    +
    + + + +
    + + {/* 完了済みTODOをすべて削除 */} + {completedCount > 0 && ( + + )} +
    + +// フィルタリング結果の表示 +
    + {getFilteredTodos().length === 0 ? ( +

    + {filter === 'active' && 'すべて完了しました!'} + {filter === 'completed' && '完了したTODOがありません'} + {filter === 'all' && 'TODOがありません'} +

    + ) : ( + getFilteredTodos().map((todo) => ( + // 通常のTODOアイテム表示 +
    + {/* 省略:通常のTODOアイテム */} +
    + )) + )} +
    +``` + +**📚 このステップの参考文献:** + +- [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`フックの理解 +- データの保存と読み込み -### Phase 5: 仕上げとベストプラクティス (2-3時間) - -#### Step 5.1: コンポーネントの分割 - -- 再利用可能なコンポーネント化 -- propsの型定義 - -#### Step 5.2: エラーハンドリング - -- 入力検証 -- エラーメッセージの表示 - -#### Step 5.3: パフォーマンス最適化 - -- `useCallback`の基礎 -- キー属性の重要性 - -## 各ステップの実装詳細 - -### Step 1.1 実装例 +**💡 詳細な実装と解説:** ```typescript -// src/app/page.tsx +import { useState, useEffect } from 'react'; + +// Local Storage のキー +const STORAGE_KEY = 'todo-app-data'; + export default function Home() { + const [todos, setTodos] = useState([]); + 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([]); + + // 初期データの読み込み + 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) => { + 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 ( -
    -

    私のTODOアプリ

    +
    +
    + {/* ヘッダー部分 */} +
    +

    TODOアプリ

    +
    + + +
    +
    + + {/* 残りのコンポーネント */} +
    ); } ``` -### Step 2.2 実装例 +**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 -// TODOの状態管理 -const [todos, setTodos] = useState([]); -``` +// 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); -```typescript -// src/app/page.tsx -'use client'; // ← 重要!状態管理を使う時は必須 + const handleSave = () => { + if (editText.trim() === '') { + alert('TODOが空です'); + return; + } + onEdit(todo.id, editText); + setIsEditing(false); + }; -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([]); - - // 入力フォームの状態も管理 - const [inputValue, setInputValue] = useState(''); + const handleCancel = () => { + setEditText(todo.text); + setIsEditing(false); + }; return ( -
    -

    TODOアプリ

    +
    + onToggle(todo.id)} + className="mr-3 h-5 w-5 cursor-pointer" + /> - {/* 入力フォーム */} -
    + {isEditing ? ( + 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 + /> + ) : ( + 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} + + )} + + {isEditing ? ( +
    + + +
    + ) : ( +
    + + +
    + )} +
    + ); +} + +// 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 ( +
    + {todos.length === 0 ? ( +

    TODOがありません

    + ) : ( + todos.map((todo) => ( + + )) + )} +
    + ); +} + +// 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 ( +
    + 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" + /> + +
    + ); +} + +// メインコンポーネント +import { AddTodoForm } from './components/AddTodoForm'; +import { TodoList } from './components/TodoList'; + +export default function Home() { + const [todos, setTodos] = useState([]); + + 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 ( +
    +
    +

    TODOアプリ

    + + +
    +
    + ); +} +``` + +**コンポーネント分割のメリット:** + +- 単一責任の原則に従う +- 再利用性が高まる +- テストが書きやすくなる +- コードが読みやすくなる + +**📚 このステップの参考文献:** + +- [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 { + 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 ( +
    +
    +

    エラーが発生しました

    +

    + 申し訳ございません。予期しないエラーが発生しました。 +

    + +
    +
    + ); + } + + return this.props.children; + } +} + +// カスタムフックでエラー処理 +const useErrorHandler = () => { + const [error, setError] = useState(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 }) => ( +
    + {message} + +
    +); + +// 入力検証の強化 +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 ( +
    + {error && } +
    setInputValue(e.target.value)} - className="border p-2 rounded" - placeholder="TODOを入力" + 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' + }`} /> -
    - - {/* TODOリスト表示 */} -
      - {todos.map((todo) => ( -
    • - {todo.text} -
    • - ))} -
    -
    + + +
    ); } ``` -**解説:** +**📚 このステップの参考文献:** -- `'use client'` - クライアントサイドで実行するコンポーネントであることを宣言 -- `useState([])` - 空の配列で初期化、型はTodoの配列 -- `onChange` - 入力が変更されるたびに実行される -- `map` - 配列の各要素をJSXに変換 -- `key={todo.id}` - Reactが効率的に更新するために必要 +- [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([]); + const [filter, setFilter] = useState('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 ( +
    +
    +

    + TODOアプリ ({stats.total}) +

    + + + + + + +
    +
    + ); +} + +// React.memoでコンポーネントをメモ化 +const FilterButtons = React.memo(({ filter, onFilterChange, stats }: { + filter: FilterType; + onFilterChange: (filter: FilterType) => void; + stats: { total: number; active: number; completed: number }; +}) => { + return ( +
    + + + +
    + ); +}); +``` + +**パフォーマンス最適化のポイント:** + +- `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のエラー -- 型定義が分からない → 最初はanyを使い、後から具体的な型に修正 -- 型推論に頼る → VSCodeの補完機能を活用 - -**💡 よくあるTypeScriptエラーと解決策:** +**よくあるエラーと解決策:** ```typescript // エラー例1: Parameter 'e' implicitly has an 'any' type @@ -558,7 +1491,7 @@ if (todo) { } ``` -**VSCodeの便利機能:** +**VSCodeの便利機能:** - 変数にカーソルを合わせると型が表示される - Ctrl+Space で補完候補を表示 @@ -566,23 +1499,56 @@ if (todo) { ### 2. 状態更新のイミュータビリティ -- 配列の直接変更はNG → スプレッド構文やfilter/mapを使用 +**よくある間違いと正しい書き方:** ```typescript -// NG -todos.push(newTodo); +// ❌ NGパターン +todos.push(newTodo); // 配列を直接変更 +todos[0].completed = true; // オブジェクトを直接変更 -// OK -setTodos([...todos, newTodo]); +// ✅ OKパターン +setTodos([...todos, newTodo]); // 新しい配列を作成 +setTodos(todos.map(todo => + todo.id === id ? { ...todo, completed: true } : todo +)); ``` -### 3. イベントハンドラーの型 +### 3. useEffectの依存配列 + +**よくある間違い:** ```typescript +// ❌ 依存配列を忘れる(無限ループの原因) +useEffect(() => { + setTodos(todos.filter(todo => todo.completed)); +}); // 依存配列がない + +// ✅ 正しい書き方 +useEffect(() => { + // 何らかの処理 +}, [todos]); // 依存配列を指定 +``` + +### 4. イベントハンドラーの型 + +**正しい型定義:** + +```typescript +// フォーム送信 const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); // 処理 }; + +// 入力変更 +const handleChange = (e: React.ChangeEvent) => { + setInputValue(e.target.value); +}; + +// ボタンクリック +const handleClick = (e: React.MouseEvent) => { + // 処理 +}; ``` ## 評価基準 @@ -593,35 +1559,60 @@ const handleSubmit = (e: React.FormEvent) => { - [ ] TODOの完了/未完了の切り替えができる - [ ] TODOの削除ができる - [ ] 適切な型定義がされている +- [ ] エラーハンドリングが実装されている ### 追加機能 (推奨) - [ ] TODOの編集ができる - [ ] フィルタリング機能がある - [ ] データが永続化される -- [ ] レスポンシブデザイン +- [ ] レスポンシブデザインに対応 +- [ ] コンポーネントが適切に分割されている ### コード品質 -- [ ] コンポーネントが適切に分割されている -- [ ] エラーハンドリングがされている -- [ ] コードが読みやすい +- [ ] TypeScriptの型安全性が確保されている +- [ ] コードが読みやすく保守性が高い +- [ ] 適切なエラーハンドリングが実装されている +- [ ] パフォーマンスが考慮されている ## 発展課題 1. **ドラッグ&ドロップ**での並び替え + - react-beautiful-dndライブラリの使用 + - カスタムフックでの実装 + 2. **カテゴリー機能**の追加 + - TODOのカテゴリ分類 + - カテゴリー別フィルタリング + 3. **期限設定**機能 + - 期限日の設定 + - 期限切れの警告表示 + 4. **Next.js API Routes**を使ったバックエンド実装 + - データベースとの連携 + - RESTful APIの実装 + 5. **認証機能**の追加(NextAuth.js) + - ユーザー登録・ログイン + - ユーザー別TODOの管理 + +6. **PWA(Progressive Web App)対応** + - オフライン対応 + - プッシュ通知 + +7. **テストの実装** + - Jest + React Testing Library + - E2Eテスト(Playwright) ## 推奨リソース ### 📚 公式ドキュメント - [Next.js公式ドキュメント](https://nextjs.org/docs) -- [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html) -- [React公式チュートリアル](https://react.dev/learn) +- [React公式ドキュメント](https://ja.react.dev/) +- [TypeScript Handbook](https://www.typescriptlang.org/docs/) - [Tailwind CSS公式ドキュメント](https://tailwindcss.com/docs) ### 🎥 日本語の動画教材 @@ -629,12 +1620,14 @@ const handleSubmit = (e: React.FormEvent) => { - [【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実践プログラミング」(マイナビ出版) @@ -643,6 +1636,13 @@ const handleSubmit = (e: React.FormEvent) => { - [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) ## サポート方法 @@ -651,11 +1651,7 @@ const handleSubmit = (e: React.FormEvent) => { 3. **デバッグセッション**: エラーの読み方を一緒に学習 4. **ベストプラクティス共有**: より良い書き方を随時提案 ---- - -このカリキュラムは柔軟に調整可能です。学習者のペースに合わせて進めてください。 - -## 👨‍🏫 学習の進め方アドバイス +## 学習の進め方アドバイス ### 初心者がつまずきやすいポイント @@ -671,6 +1667,11 @@ const handleSubmit = (e: React.FormEvent) => { - 最初はサンプルコードをそのまま写す - 動いたら少しずつ変更して実験 +4. **質問の仕方を覚える** + - 何をしようとしているのか + - 何が起きているのか(エラーメッセージ) + - 何を期待しているのか + ### 各Phaseの確認ポイント #### Phase 1 終了時 @@ -691,19 +1692,39 @@ const handleSubmit = (e: React.FormEvent) => { - [ ] TODOを完了/未完了に切り替えられる - [ ] TODOを削除できる -## 🔥 FAQ(よくある質問) +#### Phase 4 終了時 + +- [ ] TODOを編集できる +- [ ] フィルタリング機能が動作する +- [ ] データが永続化される + +#### Phase 5 終了時 + +- [ ] コンポーネントが適切に分割されている +- [ ] エラーハンドリングが実装されている +- [ ] パフォーマンスが最適化されている + +## FAQ(よくある質問) ### Q: `'use client'`を忘れてエラーが出ました -**A:** useStateなどのフックを使う時は、ファイルの先頭に`'use client'`が必要です。 +**A:** useStateなどのReact Hooksを使う時は、ファイルの先頭に`'use client'`が必要です。Next.js 13+ App Routerでは、デフォルトでServer Componentとして動作するためです。 ### Q: 「Cannot find module」エラーが出ます -**A:** importのパスが間違っている可能性があります。`./`で始まる相対パスか、`@/`で始まるエイリアスを確認してください。 +**A:** importのパスが間違っている可能性があります。 + +- `./`で始まる相対パス +- `@/`で始まるエイリアス(tsconfig.jsonで設定) +- ファイル名の拡張子を確認 ### Q: スタイルが適用されません -**A:** classNameを使っているか確認してください(classではなく)。また、Tailwind CSSのクラス名が正しいか公式ドキュメントで確認しましょう。 +**A:** + +- `className`を使っているか確認(`class`ではない) +- Tailwind CSSのクラス名が正しいか確認 +- `globals.css`がimportされているか確認 ### Q: TODOが保存されずリロードすると消えます @@ -711,11 +1732,43 @@ const handleSubmit = (e: React.FormEvent) => { ### Q: TypeScriptの型エラーが難しいです -**A:** 最初は`any`型を使ってもOKです。動くようになったら徐々に適切な型に置き換えていきましょう。 +**A:** + +- 最初は`any`型を使ってもOK +- VSCodeの型情報を活用 +- 公式ドキュメントで型を確認 +- 段階的に適切な型に置き換え ### Q: mapやfilterがよく分かりません -**A:** これらはJavaScriptの配列メソッドです。MDNの日本語ドキュメントで詳しく解説されています。 +**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:** + +- React:UIライブラリ +- Next.js:Reactベースのフレームワーク +- Next.jsはReactに以下を追加: + - ルーティング + - SSR/SSG + - API Routes + - 最適化機能 + +--- + +このカリキュラムは柔軟に調整可能です。学習者のペースに合わせて進めてください。 + +**🎯 最終目標:実用的なTODOアプリを完成させ、React/Next.js/TypeScriptの基本をマスターする**