# 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 (

Hello World!

Reactコンポーネントの第一歩です

); } ``` **解説:** - `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 (

私のTODOアプリ

); } ``` **よくあるエラーと対処法:** - `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; // 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追加関数の実装 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) => { e.preventDefault(); // ページのリロードを防ぐ addTodo(); }; // JSX部分
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" />
``` **なぜこのように書くのか:** - `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リストの表示部分
{todos.map((todo) => (
toggleTodo(todo.id)} 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); 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リストに削除ボタンを追加
{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 && (

本当に削除しますか?

)} ``` **📚 このステップの参考文献:** - [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(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リストの表示部分
{todos.map((todo) => (
toggleTodo(todo.id)} className="mr-3 h-5 w-5 cursor-pointer" /> {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`フックの理解 - データの保存と読み込み **💡 詳細な実装と解説:** ```typescript 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アプリ

{/* 残りのコンポーネント */}
); } ``` **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 (
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)} 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' }`} />
); } ``` **📚 このステップの参考文献:** - [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のエラー **よくあるエラーと解決策:** ```typescript // エラー例1: Parameter 'e' implicitly has an 'any' type // ❌ エラーが出るコード const handleChange = (e) => { setInputValue(e.target.value); }; // ✅ 解決策1: 型を明示的に指定 const handleChange = (e: React.ChangeEvent) => { setInputValue(e.target.value); }; // ✅ 解決策2: インラインで書く(型推論が効く) 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) => { e.preventDefault(); // 処理 }; // 入力変更 const handleChange = (e: React.ChangeEvent) => { setInputValue(e.target.value); }; // ボタンクリック const handleClick = (e: React.MouseEvent) => { // 処理 }; ``` ## 評価基準 ### 基本機能 (必須) - [ ] TODOの追加ができる - [ ] 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) - [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:** - React:UIライブラリ - Next.js:Reactベースのフレームワーク - Next.jsはReactに以下を追加: - ルーティング - SSR/SSG - API Routes - 最適化機能 --- このカリキュラムは柔軟に調整可能です。学習者のペースに合わせて進めてください。 **🎯 最終目標:実用的なTODOアプリを完成させ、React/Next.js/TypeScriptの基本をマスターする**