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を動かすために必要です。
インストール手順:
- Node.js公式サイトにアクセス
- LTS版(推奨版)をダウンロード
- インストーラーを実行し、すべて「次へ」で進める
インストール確認:
ターミナル(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-4xlやbg-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>
)
}
🔍 コードの解説:
-
'use client'- Next.js 16で必須- ブラウザで動く機能(useStateなど)を使うときに必要
- 書かないとエラーになります
-
useState- 状態管理のフックconst [todos, setTodos] = useState(初期値)-
todos: 現在の状態(Todoのリスト) -
setTodos: 状態を更新する関数 -
初期値: 最初に表示するデータ
-
-
mapメソッド - 配列を繰り返し処理todos.map((todo) => ...)- 配列の各要素に対して処理を実行
- HTMLを生成するのによく使います
-
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>
)
}
🔍 重要なポイント解説:
-
スプレッド構文
...setTodos([...todos, newTodo])-
...todos: 既存の配列を展開 - その後ろに
newTodoを追加 - 元の配列を変更せず、新しい配列を作る(Reactの鉄則)
-
-
trim()メソッドif (inputValue.trim() === '')- 前後の空白を削除
- 空白だけの入力を防ぐ
-
Date.now()- 現在時刻をミリ秒で取得
- 簡易的なユニークIDとして使用
- 本格的なアプリでは UUID などを使います
-
条件付きレンダリング
{todos.length === 0 ? ( <p>Todoがありません</p> ) : ( <ul>...</ul> )}- Todoが0個なら「ありません」を表示
- 1個以上ならリストを表示
✅ 動作確認:
- 入力欄に文字を入力
- 「追加」ボタンをクリック(またはEnterキー)
- リストに追加される!
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を作った会社が提供する、無料で使えるホスティングサービスです。
デプロイ手順:
-
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 -
Vercelにアクセス
- vercel.comにアクセス
- GitHubでログイン
-
プロジェクトをインポート
- 「Add New」→「Project」をクリック
- GitHubリポジトリを選択
- 「Deploy」をクリック
-
デプロイ完了!
- 数分で完成
-
https://あなたのアプリ名.vercel.appのようなURLが発行されます
🎉 おめでとうございます!世界中からアクセスできるTodoアプリが完成しました!
🎓 まとめと次のステップ
学んだこと
✅ Next.js 16のプロジェクト作成
✅ 'use client'ディレクティブの使い方
✅ useStateによる状態管理
✅ useEffectによるライフサイクル管理
✅ 配列操作(map, filter, スプレッド構文)
✅ 条件付きレンダリング
✅ イベントハンドリング
✅ LocalStorageによるデータ永続化
✅ Tailwind CSSによるスタイリング
✅ プロダクションビルドとデプロイ
🚀 次に挑戦してみよう
-
フィルター機能を追加
- 「全て」「未完了」「完了」でフィルタリング
-
編集機能を追加
- Todoの内容を後から編集できるように
-
カテゴリ機能
- 「仕事」「プライベート」などのカテゴリ分け
-
期限機能
- 各Todoに期限を設定し、期限切れを警告
-
バックエンドと連携
- SupabaseやFirebaseを使ってデータベースに保存
-
認証機能
- ログイン機能を追加して、ユーザーごとにTodoを管理
🔧 トラブルシューティング
エラー: useState is not defined
→ 'use client'をファイルの先頭に追加してください
エラー: localStorage is not defined
→ useEffectの中でLocalStorageを使ってください
スタイルが反映されない
→ tailwind.config.jsが正しく設定されているか確認してください
ビルドエラー
→ npm run buildの出力を確認し、エラーメッセージを読んでください
お疲れ様でした!これで初学者から一歩進んで、実践的なNext.jsアプリが作れるようになりました!🎉
次はこのアプリを基に、自分だけのオリジナル機能を追加してみてください。プログラミングは作って学ぶのが一番です!
Discussion