👏

Next.js ToDoチュートリアル

に公開

📘 Next.js 16でTodoアプリを作る完全ガイド

🎯 このチュートリアルの目標

  • プログラミング初学者でも挫折せずに最後まで完成できる
  • 環境構築から公開まで一貫した成功体験を得る
  • Next.js 16の基本を実践的に習得する
  • 思考しながら学び、応用力を身につける

📚 チュートリアル構成

Chapter 1: 環境準備とプロジェクト作成

  • Node.jsのインストール
  • Next.js 16プロジェクトの作成
  • 開発サーバーの起動と確認

Chapter 2: Next.jsの基本構造を理解する

  • appディレクトリの役割
  • ファイルシステムルーティング
  • layoutとpageの違い

Chapter 3: Todoリストの表示機能を作る

  • コンポーネントの作成
  • 状態管理(useState)の基本
  • データの表示

Chapter 4: Todo追加機能を実装する

  • フォームの作成
  • イベントハンドリング
  • 配列の操作

Chapter 5: Todo削除・完了機能を追加する

  • 削除ボタンの実装
  • 完了チェック機能
  • 条件付きスタイリング

Chapter 6: デザインを整える

  • TailwindCSSでスタイリング
  • レスポンシブデザイン
  • ダークモード対応

Chapter 7: データの永続化

  • LocalStorageの使用
  • useEffectの理解
  • データの保存と読み込み

Chapter 8: ビルドとデプロイ

  • プロダクションビルド
  • Vercelへのデプロイ
  • 完成とまとめ

📖 Chapter 1: 環境準備とプロジェクト作成

1-1. 必要なツールをインストールする

Node.jsのインストール

Node.jsとは?
JavaScriptをブラウザの外でも実行できるようにするツールです。Next.jsを動かすために必要です。

インストール手順:

  1. Node.js公式サイトにアクセス
  2. LTS版(推奨版)をダウンロード
  3. インストーラーを実行し、すべて「次へ」で進める

インストール確認:

ターミナル(WindowsならコマンドプロンプトまたはPowerShell、macOSならターミナル)を開いて以下を実行:

node -v
npm -v

バージョン番号が表示されればOKです!

v20.11.0  # Node.jsのバージョン例
10.2.4    # npmのバージョン例

1-2. Next.js 16プロジェクトを作成する

プロジェクトの作成

ターミナルで作業したいフォルダに移動してから実行:

npx create-next-app@latest my-todo-app

いくつか質問されるので、以下のように答えてください:

✔ Would you like to use TypeScript? … No
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like your code inside a `src/` directory? … No
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to use Turbopack for `next dev`? … Yes
✔ Would you like to customize the import alias? … No

各質問の意味:

  • TypeScript: 型チェック機能(今回は使わない)
  • ESLint: コードのエラーチェック(使う)
  • Tailwind CSS: スタイリング用(使う)
  • src/ディレクトリ: コード整理用(今回は使わない)
  • App Router: Next.js 16の推奨ルーティング(使う)
  • Turbopack: 高速なビルドツール(使う)

プロジェクトフォルダに移動

cd my-todo-app

1-3. 開発サーバーを起動する

npm run dev

ターミナルに以下のように表示されます:

▲ Next.js 16.0.0
- Local:        http://localhost:3000
- Network:      http://192.168.x.x:3000

✓ Starting...
✓ Ready in 1.2s

ブラウザで確認:

http://localhost:3000 を開くと、Next.jsのウェルカムページが表示されます!

🎉 おめでとうございます!環境構築が完了しました!


📖 Chapter 2: Next.jsの基本構造を理解する

2-1. プロジェクト構造を見てみる

作成されたフォルダを開くと、以下のような構造になっています:

my-todo-app/
├── app/              # アプリケーションのメインコード
│   ├── favicon.ico
│   ├── globals.css   # グローバルスタイル
│   ├── layout.js     # レイアウト(全ページ共通)
│   └── page.js       # トップページ
├── public/           # 画像などの静的ファイル
├── node_modules/     # インストールされたパッケージ
├── package.json      # プロジェクト設定
└── next.config.js    # Next.js設定

重要なのは app フォルダです! ここにすべてのコードを書いていきます。


2-2. ファイルの役割を理解する

app/layout.js - 全ページ共通のレイアウト

現在の内容を見てみましょう:

import './globals.css'

export const metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}

このファイルの役割:

  • すべてのページで共通のHTML構造を定義
  • {children} の部分に各ページの内容が入る
  • ヘッダーやフッターなど、全ページ共通の要素を書く場所

app/page.js - トップページ

このファイルが http://localhost:3000/ に表示される内容です。

Next.js 16の重要なポイント:

  • ファイル名が page.js → そのフォルダのページになる
  • フォルダ構造がそのままURL構造になる(ファイルシステムルーティング)

2-3. 最初のカスタマイズをしてみる

app/page.js を以下のように完全に書き換えてください:

export default function Home() {
  return (
    <main className="min-h-screen bg-gray-50 py-8">
      <div className="max-w-2xl mx-auto">
        <h1 className="text-4xl font-bold text-center text-gray-800 mb-8">
          📝 My Todo App
        </h1>
        <div className="bg-white rounded-lg shadow-md p-6">
          <p className="text-gray-600 text-center">
            これからTodoアプリを作っていきます!
          </p>
        </div>
      </div>
    </main>
  )
}

保存してブラウザを確認してください。 自動でページがリロードされ、新しい内容が表示されます!

🤔 考えてみよう:

  • className に書かれている text-4xlbg-white は何?
    → これはTailwind CSSのクラス名です。次のChapterで詳しく学びます。
  • なぜ保存するだけでページが更新される?
    → Next.jsのHot Module Replacement (HMR) 機能です。開発中は自動で変更を反映してくれます。

📖 Chapter 3: Todoリストの表示機能を作る

3-1. Reactの状態管理を理解する

状態(State)とは?
アプリケーション内で変化するデータのことです。Todoアプリでは、「Todoのリスト」が状態として管理されます。

app/page.js を以下のように書き換えます:

'use client'  // これがないとuseStateが使えません

import { useState } from 'react'

export default function Home() {
  // 状態の定義:todosという変数と、それを更新するsetTodos関数
  const [todos, setTodos] = useState([
    { id: 1, text: '最初のTodo', completed: false },
    { id: 2, text: 'Next.jsを学ぶ', completed: false },
    { id: 3, text: 'Todoアプリを完成させる', completed: false },
  ])

  return (
    <main className="min-h-screen bg-gray-50 py-8">
      <div className="max-w-2xl mx-auto">
        <h1 className="text-4xl font-bold text-center text-gray-800 mb-8">
          📝 My Todo App
        </h1>
        
        <div className="bg-white rounded-lg shadow-md p-6">
          <ul className="space-y-2">
            {todos.map((todo) => (
              <li 
                key={todo.id}
                className="p-3 bg-gray-50 rounded hover:bg-gray-100"
              >
                {todo.text}
              </li>
            ))}
          </ul>
        </div>
      </div>
    </main>
  )
}

🔍 コードの解説:

  1. 'use client' - Next.js 16で必須

    • ブラウザで動く機能(useStateなど)を使うときに必要
    • 書かないとエラーになります
  2. useState - 状態管理のフック

    const [todos, setTodos] = useState(初期値)
    
    • todos: 現在の状態(Todoのリスト)
    • setTodos: 状態を更新する関数
    • 初期値: 最初に表示するデータ
  3. mapメソッド - 配列を繰り返し処理

    todos.map((todo) => ...)
    
    • 配列の各要素に対して処理を実行
    • HTMLを生成するのによく使います
  4. key プロパティ - Reactの必須項目

    • リストの各要素を識別するための一意なID
    • 書かないと警告が出ます

3-2. データ構造を理解する

現在、todosは以下のような構造です:

[
  { id: 1, text: '最初のTodo', completed: false },
  { id: 2, text: 'Next.jsを学ぶ', completed: false },
  { id: 3, text: 'Todoアプリを完成させる', completed: false },
]

各プロパティの意味:

  • id: 各Todoを識別するための番号(重複しない)
  • text: Todoの内容
  • completed: 完了しているかどうか(trueかfalse)

🤔 考えてみよう:

  • なぜidが必要?
    → 削除や編集のときに、どのTodoか特定するため
  • completedを今は使っていないけど、後で何に使う?
    → 完了チェック機能で使います(Chapter 5)

📖 Chapter 4: Todo追加機能を実装する

4-1. 入力フォームを作る

app/page.js を以下のように更新します:

'use client'

import { useState } from 'react'

export default function Home() {
  const [todos, setTodos] = useState([
    { id: 1, text: '最初のTodo', completed: false },
    { id: 2, text: 'Next.jsを学ぶ', completed: false },
    { id: 3, text: 'Todoアプリを完成させる', completed: false },
  ])

  // 新しいTodoの入力内容を管理する状態
  const [inputValue, setInputValue] = useState('')

  // Todoを追加する関数
  const addTodo = () => {
    // 入力が空なら何もしない
    if (inputValue.trim() === '') return

    // 新しいTodoオブジェクトを作成
    const newTodo = {
      id: Date.now(), // 現在時刻をIDにする(簡易的な方法)
      text: inputValue,
      completed: false,
    }

    // 既存のtodosに新しいTodoを追加
    setTodos([...todos, newTodo])

    // 入力欄をクリア
    setInputValue('')
  }

  // Enterキーで追加できるようにする
  const handleKeyPress = (e) => {
    if (e.key === 'Enter') {
      addTodo()
    }
  }

  return (
    <main className="min-h-screen bg-gray-50 py-8">
      <div className="max-w-2xl mx-auto">
        <h1 className="text-4xl font-bold text-center text-gray-800 mb-8">
          📝 My Todo App
        </h1>
        
        {/* 入力フォーム */}
        <div className="bg-white rounded-lg shadow-md p-6 mb-4">
          <div className="flex gap-2">
            <input
              type="text"
              value={inputValue}
              onChange={(e) => setInputValue(e.target.value)}
              onKeyPress={handleKeyPress}
              placeholder="新しいTodoを入力..."
              className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
            />
            <button
              onClick={addTodo}
              className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
            >
              追加
            </button>
          </div>
        </div>

        {/* Todoリスト */}
        <div className="bg-white rounded-lg shadow-md p-6">
          {todos.length === 0 ? (
            <p className="text-gray-400 text-center py-8">
              Todoがありません。追加してみましょう!
            </p>
          ) : (
            <ul className="space-y-2">
              {todos.map((todo) => (
                <li 
                  key={todo.id}
                  className="p-3 bg-gray-50 rounded hover:bg-gray-100"
                >
                  {todo.text}
                </li>
              ))}
            </ul>
          )}
        </div>
      </div>
    </main>
  )
}

🔍 重要なポイント解説:

  1. スプレッド構文 ...

    setTodos([...todos, newTodo])
    
    • ...todos: 既存の配列を展開
    • その後ろに newTodo を追加
    • 元の配列を変更せず、新しい配列を作る(Reactの鉄則)
  2. trim() メソッド

    if (inputValue.trim() === '')
    
    • 前後の空白を削除
    • 空白だけの入力を防ぐ
  3. Date.now()

    • 現在時刻をミリ秒で取得
    • 簡易的なユニークIDとして使用
    • 本格的なアプリでは UUID などを使います
  4. 条件付きレンダリング

    {todos.length === 0 ? (
      <p>Todoがありません</p>
    ) : (
      <ul>...</ul>
    )}
    
    • Todoが0個なら「ありません」を表示
    • 1個以上ならリストを表示

✅ 動作確認:

  1. 入力欄に文字を入力
  2. 「追加」ボタンをクリック(またはEnterキー)
  3. リストに追加される!

4-2. コードを整理する(リファクタリング)

🤔 考えてみよう:
今のコードでも動きますが、関数が増えてきました。読みやすくするために、関数を上の方にまとめましょう。

'use client'

import { useState } from 'react'

export default function Home() {
  // ========== 状態管理 ==========
  const [todos, setTodos] = useState([
    { id: 1, text: '最初のTodo', completed: false },
    { id: 2, text: 'Next.jsを学ぶ', completed: false },
    { id: 3, text: 'Todoアプリを完成させる', completed: false },
  ])
  const [inputValue, setInputValue] = useState('')

  // ========== 関数定義 ==========
  const addTodo = () => {
    if (inputValue.trim() === '') return
    const newTodo = {
      id: Date.now(),
      text: inputValue,
      completed: false,
    }
    setTodos([...todos, newTodo])
    setInputValue('')
  }

  const handleKeyPress = (e) => {
    if (e.key === 'Enter') addTodo()
  }

  // ========== 画面表示 ==========
  return (
    // ... 同じコード
  )
}

このように、コードをセクションごとに分けると読みやすくなります!


📖 Chapter 5: Todo削除・完了機能を追加する

5-1. 削除機能を実装する

'use client'

import { useState } from 'react'

export default function Home() {
  const [todos, setTodos] = useState([
    { id: 1, text: '最初のTodo', completed: false },
    { id: 2, text: 'Next.jsを学ぶ', completed: false },
    { id: 3, text: 'Todoアプリを完成させる', completed: false },
  ])
  const [inputValue, setInputValue] = useState('')

  const addTodo = () => {
    if (inputValue.trim() === '') return
    const newTodo = {
      id: Date.now(),
      text: inputValue,
      completed: false,
    }
    setTodos([...todos, newTodo])
    setInputValue('')
  }

  // 削除機能を追加
  const deleteTodo = (id) => {
    setTodos(todos.filter((todo) => todo.id !== id))
  }

  const handleKeyPress = (e) => {
    if (e.key === 'Enter') addTodo()
  }

  return (
    <main className="min-h-screen bg-gray-50 py-8">
      <div className="max-w-2xl mx-auto">
        <h1 className="text-4xl font-bold text-center text-gray-800 mb-8">
          📝 My Todo App
        </h1>
        
        <div className="bg-white rounded-lg shadow-md p-6 mb-4">
          <div className="flex gap-2">
            <input
              type="text"
              value={inputValue}
              onChange={(e) => setInputValue(e.target.value)}
              onKeyPress={handleKeyPress}
              placeholder="新しいTodoを入力..."
              className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
            />
            <button
              onClick={addTodo}
              className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
            >
              追加
            </button>
          </div>
        </div>

        <div className="bg-white rounded-lg shadow-md p-6">
          {todos.length === 0 ? (
            <p className="text-gray-400 text-center py-8">
              Todoがありません。追加してみましょう!
            </p>
          ) : (
            <ul className="space-y-2">
              {todos.map((todo) => (
                <li 
                  key={todo.id}
                  className="flex items-center justify-between p-3 bg-gray-50 rounded hover:bg-gray-100"
                >
                  <span>{todo.text}</span>
                  <button
                    onClick={() => deleteTodo(todo.id)}
                    className="px-3 py-1 bg-red-500 text-white text-sm rounded hover:bg-red-600 transition-colors"
                  >
                    削除
                  </button>
                </li>
              ))}
            </ul>
          )}
        </div>
      </div>
    </main>
  )
}

🔍 削除機能の仕組み:

const deleteTodo = (id) => {
  setTodos(todos.filter((todo) => todo.id !== id))
}
  • filter(): 条件に合う要素だけを残す
  • todo.id !== id: 削除したいID以外を残す
  • 結果として、指定したIDのTodoが削除される

5-2. 完了チェック機能を実装する

'use client'

import { useState } from 'react'

export default function Home() {
  const [todos, setTodos] = useState([
    { id: 1, text: '最初のTodo', completed: false },
    { id: 2, text: 'Next.jsを学ぶ', completed: false },
    { id: 3, text: 'Todoアプリを完成させる', completed: false },
  ])
  const [inputValue, setInputValue] = useState('')

  const addTodo = () => {
    if (inputValue.trim() === '') return
    const newTodo = {
      id: Date.now(),
      text: inputValue,
      completed: false,
    }
    setTodos([...todos, newTodo])
    setInputValue('')
  }

  const deleteTodo = (id) => {
    setTodos(todos.filter((todo) => todo.id !== id))
  }

  // 完了/未完了を切り替える機能
  const toggleTodo = (id) => {
    setTodos(
      todos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    )
  }

  const handleKeyPress = (e) => {
    if (e.key === 'Enter') addTodo()
  }

  return (
    <main className="min-h-screen bg-gray-50 py-8">
      <div className="max-w-2xl mx-auto">
        <h1 className="text-4xl font-bold text-center text-gray-800 mb-8">
          📝 My Todo App
        </h1>
        
        <div className="bg-white rounded-lg shadow-md p-6 mb-4">
          <div className="flex gap-2">
            <input
              type="text"
              value={inputValue}
              onChange={(e) => setInputValue(e.target.value)}
              onKeyPress={handleKeyPress}
              placeholder="新しいTodoを入力..."
              className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
            />
            <button
              onClick={addTodo}
              className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
            >
              追加
            </button>
          </div>
        </div>

        <div className="bg-white rounded-lg shadow-md p-6">
          {todos.length === 0 ? (
            <p className="text-gray-400 text-center py-8">
              Todoがありません。追加してみましょう!
            </p>
          ) : (
            <ul className="space-y-2">
              {todos.map((todo) => (
                <li 
                  key={todo.id}
                  className="flex items-center gap-3 p-3 bg-gray-50 rounded hover:bg-gray-100"
                >
                  <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-400' 
                        : 'text-gray-800'
                    }`}
                  >
                    {todo.text}
                  </span>
                  <button
                    onClick={() => deleteTodo(todo.id)}
                    className="px-3 py-1 bg-red-500 text-white text-sm rounded hover:bg-red-600 transition-colors"
                  >
                    削除
                  </button>
                </li>
              ))}
            </ul>
          )}
        </div>
      </div>
    </main>
  )
}

🔍 完了機能の仕組み:

const toggleTodo = (id) => {
  setTodos(
    todos.map((todo) =>
      todo.id === id 
        ? { ...todo, completed: !todo.completed }  // IDが一致したら反転
        : todo  // それ以外はそのまま
    )
  )
}
  • map(): 配列の各要素を変換
  • { ...todo, completed: !todo.completed }:
    • スプレッド構文で既存のプロパティをコピー
    • completed だけを反転(true → false、false → true)

条件付きスタイリング:

className={`flex-1 ${
  todo.completed 
    ? 'line-through text-gray-400'  // 完了なら取り消し線
    : 'text-gray-800'               // 未完了なら通常
}`}

📖 Chapter 6: デザインを整える

6-1. 統計情報を追加する

Todoの総数、完了数を表示しましょう:

'use client'

import { useState } from 'react'

export default function Home() {
  const [todos, setTodos] = useState([
    { id: 1, text: '最初のTodo', completed: false },
    { id: 2, text: 'Next.jsを学ぶ', completed: false },
    { id: 3, text: 'Todoアプリを完成させる', completed: false },
  ])
  const [inputValue, setInputValue] = useState('')

  // 統計情報を計算
  const totalTodos = todos.length
  const completedTodos = todos.filter((todo) => todo.completed).length
  const activeTodos = totalTodos - completedTodos

  const addTodo = () => {
    if (inputValue.trim() === '') return
    const newTodo = {
      id: Date.now(),
      text: inputValue,
      completed: false,
    }
    setTodos([...todos, newTodo])
    setInputValue('')
  }

  const deleteTodo = (id) => {
    setTodos(todos.filter((todo) => todo.id !== id))
  }

  const toggleTodo = (id) => {
    setTodos(
      todos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    )
  }

  const handleKeyPress = (e) => {
    if (e.key === 'Enter') addTodo()
  }

  return (
    <main className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 py-8">
      <div className="max-w-2xl mx-auto px-4">
        <h1 className="text-5xl font-bold text-center text-gray-800 mb-2">
          📝 My Todo App
        </h1>
        <p className="text-center text-gray-600 mb-8">
          Next.js 16で作るシンプルなTodoアプリ
        </p>

        {/* 統計情報 */}
        <div className="grid grid-cols-3 gap-4 mb-6">
          <div className="bg-white rounded-lg shadow-md p-4 text-center">
            <p className="text-2xl font-bold text-gray-800">{totalTodos}</p>
            <p className="text-sm text-gray-600">全て</p>
          </div>
          <div className="bg-white rounded-lg shadow-md p-4 text-center">
            <p className="text-2xl font-bold text-blue-600">{activeTodos}</p>
            <p className="text-sm text-gray-600">未完了</p>
          </div>
          <div className="bg-white rounded-lg shadow-md p-4 text-center">
            <p className="text-2xl font-bold text-green-600">{completedTodos}</p>
            <p className="text-sm text-gray-600">完了</p>
          </div>
        </div>
        
        {/* 入力フォーム */}
        <div className="bg-white rounded-lg shadow-md p-6 mb-4">
          <div className="flex gap-2">
            <input
              type="text"
              value={inputValue}
              onChange={(e) => setInputValue(e.target.value)}
              onKeyPress={handleKeyPress}
              placeholder="新しいTodoを入力..."
              className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
            />
            <button
              onClick={addTodo}
              className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors font-medium"
            >
              追加
            </button>
          </div>
        </div>

        {/* Todoリスト */}
        <div className="bg-white rounded-lg shadow-md p-6">
          {todos.length === 0 ? (
            <p className="text-gray-400 text-center py-8">
              Todoがありません。追加してみましょう!
            </p>
          ) : (
            <ul className="space-y-2">
              {todos.map((todo) => (
                <li 
                  key={todo.id}
                  className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
                >
                  <input
                    type="checkbox"
                    checked={todo.completed}
                    onChange={() => toggleTodo(todo.id)}
                    className="w-5 h-5 cursor-pointer accent-blue-500"
                  />
                  <span 
                    className={`flex-1 ${
                      todo.completed 
                        ? 'line-through text-gray-400' 
                        : 'text-gray-800'
                    }`}
                  >
                    {todo.text}
                  </span>
                  <button
                    onClick={() => deleteTodo(todo.id)}
                    className="px-3 py-1 bg-red-500 text-white text-sm rounded-lg hover:bg-red-600 transition-colors"
                  >
                    🗑️ 削除
                  </button>
                </li>
              ))}
            </ul>
          )}
        </div>
      </div>
    </main>
  )
}

📖 Chapter 7: データの永続化

7-1. LocalStorageに保存する

現在、ページをリロードするとデータが消えてしまいます。LocalStorageを使って保存しましょう!

⚠️ Next.js 16の注意点:
LocalStorageはブラウザでしか動作しないため、useEffectを使って初回読み込み時のみ実行します。

'use client'

import { useState, useEffect } from 'react'

export default function Home() {
  const [todos, setTodos] = useState([])
  const [inputValue, setInputValue] = useState('')
  const [isLoaded, setIsLoaded] = useState(false)

  // 初回読み込み時にLocalStorageからデータを取得
  useEffect(() => {
    const savedTodos = localStorage.getItem('todos')
    if (savedTodos) {
      setTodos(JSON.parse(savedTodos))
    } else {
      // 初期データをセット
      setTodos([
        { id: 1, text: '最初のTodo', completed: false },
        { id: 2, text: 'Next.jsを学ぶ', completed: false },
        { id: 3, text: 'Todoアプリを完成させる', completed: false },
      ])
    }
    setIsLoaded(true)
  }, [])

  // todosが変更されたらLocalStorageに保存
  useEffect(() => {
    if (isLoaded) {
      localStorage.setItem('todos', JSON.stringify(todos))
    }
  }, [todos, isLoaded])

  const totalTodos = todos.length
  const completedTodos = todos.filter((todo) => todo.completed).length
  const activeTodos = totalTodos - completedTodos

  const addTodo = () => {
    if (inputValue.trim() === '') return
    const newTodo = {
      id: Date.now(),
      text: inputValue,
      completed: false,
    }
    setTodos([...todos, newTodo])
    setInputValue('')
  }

  const deleteTodo = (id) => {
    setTodos(todos.filter((todo) => todo.id !== id))
  }

  const toggleTodo = (id) => {
    setTodos(
      todos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    )
  }

  const handleKeyPress = (e) => {
    if (e.key === 'Enter') addTodo()
  }

  // データ読み込み中は表示しない
  if (!isLoaded) {
    return (
      <main className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center">
        <p className="text-xl text-gray-600">読み込み中...</p>
      </main>
    )
  }

  return (
    <main className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 py-8">
      <div className="max-w-2xl mx-auto px-4">
        <h1 className="text-5xl font-bold text-center text-gray-800 mb-2">
          📝 My Todo App
        </h1>
        <p className="text-center text-gray-600 mb-8">
          Next.js 16で作るシンプルなTodoアプリ
        </p>

        <div className="grid grid-cols-3 gap-4 mb-6">
          <div className="bg-white rounded-lg shadow-md p-4 text-center">
            <p className="text-2xl font-bold text-gray-800">{totalTodos}</p>
            <p className="text-sm text-gray-600">全て</p>
          </div>
          <div className="bg-white rounded-lg shadow-md p-4 text-center">
            <p className="text-2xl font-bold text-blue-600">{activeTodos}</p>
            <p className="text-sm text-gray-600">未完了</p>
          </div>
          <div className="bg-white rounded-lg shadow-md p-4 text-center">
            <p className="text-2xl font-bold text-green-600">{completedTodos}</p>
            <p className="text-sm text-gray-600">完了</p>
          </div>
        </div>
        
        <div className="bg-white rounded-lg shadow-md p-6 mb-4">
          <div className="flex gap-2">
            <input
              type="text"
              value={inputValue}
              onChange={(e) => setInputValue(e.target.value)}
              onKeyPress={handleKeyPress}
              placeholder="新しいTodoを入力..."
              className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
            />
            <button
              onClick={addTodo}
              className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors font-medium"
            >
              追加
            </button>
          </div>
        </div>

        <div className="bg-white rounded-lg shadow-md p-6">
          {todos.length === 0 ? (
            <p className="text-gray-400 text-center py-8">
              Todoがありません。追加してみましょう!
            </p>
          ) : (
            <ul className="space-y-2">
              {todos.map((todo) => (
                <li 
                  key={todo.id}
                  className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
                >
                  <input
                    type="checkbox"
                    checked={todo.completed}
                    onChange={() => toggleTodo(todo.id)}
                    className="w-5 h-5 cursor-pointer accent-blue-500"
                  />
                  <span 
                    className={`flex-1 ${
                      todo.completed 
                        ? 'line-through text-gray-400' 
                        : 'text-gray-800'
                    }`}
                  >
                    {todo.text}
                  </span>
                  <button
                    onClick={() => deleteTodo(todo.id)}
                    className="px-3 py-1 bg-red-500 text-white text-sm rounded-lg hover:bg-red-600 transition-colors"
                  >
                    🗑️ 削除
                  </button>
                </li>
              ))}
            </ul>
          )}
        </div>
      </div>
    </main>
  )
}

🔍 useEffectの仕組み:

useEffect(() => {
  // この中の処理が実行される
}, [依存配列])
  • [] が空 → 初回レンダリング時のみ実行
  • [todos] → todosが変わるたびに実行

🎉 これでページをリロードしてもデータが残ります!


📖 Chapter 8: ビルドとデプロイ

8-1. プロダクションビルドを作る

開発が完了したら、本番用にビルドします:

npm run build

ビルドが完了すると、以下のように表示されます:

Route (app)                              Size     First Load JS
┌ ○ /                                    5.23 kB        92.1 kB
└ ○ /_not-found                          871 B          87.7 kB

ローカルで本番環境を確認:

npm start

http://localhost:3000 で確認できます。


8-2. Vercelにデプロイする

Vercelとは?
Next.jsを作った会社が提供する、無料で使えるホスティングサービスです。

デプロイ手順:

  1. GitHubにコードをプッシュ

    git init
    git add .
    git commit -m "Initial commit"
    git branch -M main
    git remote add origin https://github.com/あなたのユーザー名/my-todo-app.git
    git push -u origin main
    
  2. Vercelにアクセス

  3. プロジェクトをインポート

    • 「Add New」→「Project」をクリック
    • GitHubリポジトリを選択
    • 「Deploy」をクリック
  4. デプロイ完了!

    • 数分で完成
    • https://あなたのアプリ名.vercel.app のようなURLが発行されます

🎉 おめでとうございます!世界中からアクセスできるTodoアプリが完成しました!


🎓 まとめと次のステップ

学んだこと

✅ Next.js 16のプロジェクト作成
'use client'ディレクティブの使い方
useStateによる状態管理
useEffectによるライフサイクル管理
✅ 配列操作(map, filter, スプレッド構文)
✅ 条件付きレンダリング
✅ イベントハンドリング
✅ LocalStorageによるデータ永続化
✅ Tailwind CSSによるスタイリング
✅ プロダクションビルドとデプロイ

🚀 次に挑戦してみよう

  1. フィルター機能を追加

    • 「全て」「未完了」「完了」でフィルタリング
  2. 編集機能を追加

    • Todoの内容を後から編集できるように
  3. カテゴリ機能

    • 「仕事」「プライベート」などのカテゴリ分け
  4. 期限機能

    • 各Todoに期限を設定し、期限切れを警告
  5. バックエンドと連携

    • SupabaseやFirebaseを使ってデータベースに保存
  6. 認証機能

    • ログイン機能を追加して、ユーザーごとにTodoを管理

🔧 トラブルシューティング

エラー: useState is not defined

'use client'をファイルの先頭に追加してください

エラー: localStorage is not defined

useEffectの中でLocalStorageを使ってください

スタイルが反映されない

tailwind.config.jsが正しく設定されているか確認してください

ビルドエラー

npm run buildの出力を確認し、エラーメッセージを読んでください


お疲れ様でした!これで初学者から一歩進んで、実践的なNext.jsアプリが作れるようになりました!🎉

次はこのアプリを基に、自分だけのオリジナル機能を追加してみてください。プログラミングは作って学ぶのが一番です!

Discussion