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

20 KiB
Raw Blame History

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.tsxlayout.tsxの違い
  • TypeScriptファイル.ts/.tsxの意味

Step 1.2: 最初のコンポーネント作成

  • Hello Worldコンポーネントの作成
  • JSXの基本文法
  • 型定義の初歩stringやnumberの使い方

💡 詳細な実装手順:

  1. 新しいコンポーネントファイルを作成
// 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の予約語と競合を避けるため
  1. コンポーネントを使ってみる
// 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の編集ができる
  • フィルタリング機能がある
  • データが永続化される
  • レスポンシブデザイン

コード品質

  • コンポーネントが適切に分割されている
  • エラーハンドリングがされている
  • コードが読みやすい

発展課題

  1. ドラッグ&ドロップでの並び替え
  2. カテゴリー機能の追加
  3. 期限設定機能
  4. Next.js API Routesを使ったバックエンド実装
  5. 認証機能の追加NextAuth.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の日本語ドキュメントで詳しく解説されています。