Lecture 1React環境構築 — Viteでプロジェクトを作る
12:00
React環境構築 — Viteでプロジェクトを作る
このコースでは、Anthropic が提供する AI コーディングツール「Claude Code」を使って、React の TODO アプリをゼロから構築します。第1回では、開発環境をセットアップし、Vite を使って React + TypeScript プロジェクトを作成するところから始めましょう。Claude Code にプロンプトを投げるだけで、面倒な初期設定を一瞬で終わらせることができます。
Node.js と Claude Code の準備
まず開発に必要なツールをインストールします。Node.js は React 開発に不可欠なランタイム環境です。公式サイト(https://nodejs.org/)から LTS 版をダウンロードしてインストールしてください。
インストール後、ターミナルでバージョンを確認します。
node --version
# v20.x.x 以上を推奨
npm --version
# 10.x.x 以上
次に Claude Code をインストールします。Claude Code は Anthropic の公式 CLI ツールで、ターミナル上で自然言語の指示を出すと、コードの生成・編集・実行をすべて行ってくれます。
npm install -g @anthropic-ai/claude-code
インストールが完了したら、作業ディレクトリで claude コマンドを実行して Claude Code を起動します。
mkdir todo-app-project
cd todo-app-project
claude
Claude Code が起動すると、対話型のプロンプトが表示されます。ここに自然言語で指示を入力していきます。
Vite で React プロジェクトを作成する
Claude Code に次のプロンプトを入力して、プロジェクトを作成しましょう。
> Viteを使ってReact + TypeScriptのプロジェクトを作成して。
プロジェクト名は "todo-app"
Claude Code は以下のコマンドを実行してくれます。
npm create vite@latest todo-app -- --template react-ts
cd todo-app
npm install
プロジェクトが作成されたら、ディレクトリ構成を確認してみましょう。Claude Code に聞いてみます。
> 今作ったプロジェクトのディレクトリ構成を説明して
Claude Code が生成するプロジェクトの基本構成は次のとおりです。
todo-app/
├── public/ # 静的ファイル
├── src/
│ ├── App.tsx # メインコンポーネント
│ ├── App.css # スタイル
│ ├── main.tsx # エントリポイント
│ └── vite-env.d.ts
├── index.html # HTMLテンプレート
├── package.json # 依存関係
├── tsconfig.json # TypeScript設定
└── vite.config.ts # Vite設定
src/main.tsx はアプリケーションのエントリポイントで、React の createRoot を使って App コンポーネントを DOM にマウントします。src/App.tsx がメインのコンポーネントファイルで、ここに TODO アプリの本体を構築していきます。
開発サーバーを起動する
プロジェクトが作成できたら、開発サーバーを起動して動作確認しましょう。Claude Code にお願いします。
> 開発サーバーを起動して
Claude Code は以下のコマンドを実行します。
npm run dev
ターミナルに次のような出力が表示されます。
VITE v6.x.x ready in 300 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
ブラウザで http://localhost:5173/ にアクセスすると、Vite + React のデフォルトページが表示されます。カウンターボタンが動作すれば、環境構築は成功です。
Vite の大きな特徴は HMR(Hot Module Replacement)です。ソースコードを変更すると、ブラウザが自動的にリロードされ、変更がリアルタイムに反映されます。これにより、開発中のフィードバックループが非常に短くなります。
App.tsx を TODO アプリ用に整理する
デフォルトのコードを TODO アプリの出発点として整理しましょう。Claude Code に次のプロンプトを投げます。
> App.tsxをTODOアプリの出発点として書き換えて。
シンプルに「TODOアプリ」というタイトルだけ表示するようにして。
不要なデフォルトのコードとCSSは削除して。
Claude Code が src/App.tsx を以下のように書き換えてくれます。
function App() {
return (
<div className="app">
<h1>TODOアプリ</h1>
</div>
);
}
export default App;
同時に src/App.css もクリーンな状態にしてくれます。
.app {
max-width: 600px;
margin: 0 auto;
padding: 2rem;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
h1 {
text-align: center;
color: #333;
}
ブラウザに「TODOアプリ」というタイトルだけが表示されれば成功です。これが今後の開発のベースになります。
演習問題
- 環境確認:
node --versionとnpm --versionを実行して、正しくインストールされていることを確認してください。 - Claude Code 操作: Claude Code を起動し、
package.jsonの内容を説明してもらいましょう。プロンプト例:package.jsonの中身を説明して - カスタマイズ: Claude Code に「タイトルの下に『Claude Codeで作るTODOアプリ』というサブタイトルを追加して」と依頼してみましょう。HMR でリアルタイムに変更が反映されることを確認してください。
- Vite 設定:
vite.config.tsの内容を確認し、ポート番号を 3000 に変更するプロンプトを Claude Code に投げてみましょう。
参考リンク
Lecture 2コンポーネント設計 — TODOアプリの構造を決める
12:00
コンポーネント設計 — TODOアプリの構造を決める
React アプリケーション開発で最も重要なステップの一つが、コンポーネント設計です。どのように画面を分割し、データをどこで管理するかを事前に決めておくことで、後の実装がスムーズに進みます。この講義では、Claude Code と対話しながら TODO アプリのコンポーネントツリーを設計し、型定義やデータフローを計画します。
コンポーネントツリーの設計
まず、TODO アプリに必要な UI 要素を洗い出し、それをコンポーネントに分割しましょう。Claude Code に相談します。
> TODOアプリに必要なコンポーネントを設計して。
コンポーネントツリーを図で示して。
各コンポーネントの役割も説明して。
Claude Code が提案するコンポーネントツリーは次のとおりです。
App
├── AddTodoForm # 新しいTODOを入力するフォーム
├── FilterButtons # 全て/未完了/完了の切替ボタン
└── TodoList # TODOリストの一覧
└── TodoItem # 個々のTODOアイテム(複数)
各コンポーネントの責務を明確にしておきましょう。
- App: アプリケーション全体のルートコンポーネント。TODO データの state を保持し、子コンポーネントに渡す。すべてのビジネスロジック(追加・削除・完了切替)を管理する。
- AddTodoForm: テキスト入力と送信ボタンを持つフォーム。ユーザーが入力した内容を親に通知する。
- FilterButtons: 表示フィルター(すべて・未完了・完了済み)を切り替えるボタン群。現在のフィルター状態を表示する。
- TodoList: フィルタリングされた TODO の配列を受け取り、TodoItem コンポーネントのリストをレンダリングする。
- TodoItem: 個々の TODO を表示する。チェックボックス、テキスト、編集ボタン、削除ボタンを持つ。
この設計では、「単一責任の原則」を意識しています。各コンポーネントが一つの明確な役割だけを持つことで、コードの可読性と保守性が高まります。
TypeScript の型定義
次に、TODO データの型を定義しましょう。Claude Code にお願いします。
> TODOアプリで使う型定義ファイルを作成して。
src/types/todo.ts に配置して。
Todo型にはid、text、completed、createdAtを含めて。
Claude Code が生成するコードは以下のとおりです。
// src/types/todo.ts
export interface Todo {
id: string;
text: string;
completed: boolean;
createdAt: number;
}
export type FilterType = 'all' | 'active' | 'completed';
各フィールドの役割を説明します。
- id: 各 TODO を一意に識別する文字列。
crypto.randomUUID()を使って生成します。 - text: TODO の内容テキスト。
- completed: 完了状態を表す真偽値。デフォルトは
false。 - createdAt: 作成日時のタイムスタンプ。
Date.now()で取得する数値。 - FilterType: フィルターの種類を表すユニオン型。'all'、'active'、'completed' の3つの値のみ許可します。
TypeScript の型定義を最初に作っておくことで、コンポーネント間のデータの受け渡しで型の不整合が起きるのを防げます。Claude Code も型情報を参照して正確なコードを生成してくれます。
Props と State の設計
コンポーネント間でどのようにデータを受け渡すかを設計します。Claude Code に全体像を聞いてみましょう。
> TODOアプリの各コンポーネントのpropsとstateを設計して。
型定義も含めて具体的に示して。
Claude Code が提案する設計は次のとおりです。
// App コンポーネントの State
// todos: Todo[] — TODOリスト全体
// filter: FilterType — 現在のフィルター
// AddTodoForm の Props
interface AddTodoFormProps {
onAdd: (text: string) => void;
}
// FilterButtons の Props
interface FilterButtonsProps {
currentFilter: FilterType;
onFilterChange: (filter: FilterType) => void;
counts: {
all: number;
active: number;
completed: number;
};
}
// TodoList の Props
interface TodoListProps {
todos: Todo[];
onToggle: (id: string) => void;
onDelete: (id: string) => void;
onEdit: (id: string, newText: string) => void;
}
// TodoItem の Props
interface TodoItemProps {
todo: Todo;
onToggle: (id: string) => void;
onDelete: (id: string) => void;
onEdit: (id: string, newText: string) => void;
}
React のデータフローは「トップダウン(単方向データフロー)」が基本です。state は App コンポーネントに集約し、子コンポーネントには props として渡します。子コンポーネントからの操作は、コールバック関数(onAdd、onToggle、onDelete、onEdit)を通じて親に通知します。
このパターンは「状態の持ち上げ(Lifting State Up)」と呼ばれ、React 開発で最も基本的かつ重要な設計パターンです。
コンポーネントファイルの雛形を作成する
設計が決まったら、各コンポーネントの雛形ファイルを作成しましょう。Claude Code にまとめて作ってもらいます。
> 以下のコンポーネントファイルを src/components/ に作成して。
各ファイルは最小限のコードで、propsの型定義だけ含めて。
- AddTodoForm.tsx
- FilterButtons.tsx
- TodoList.tsx
- TodoItem.tsx
Claude Code が生成する TodoItem.tsx の例を示します。
// src/components/TodoItem.tsx
import { Todo } from '../types/todo';
interface TodoItemProps {
todo: Todo;
onToggle: (id: string) => void;
onDelete: (id: string) => void;
onEdit: (id: string, newText: string) => void;
}
function TodoItem({ todo, onToggle, onDelete, onEdit }: TodoItemProps) {
return (
<li className="todo-item">
<span>{todo.text}</span>
</li>
);
}
export default TodoItem;
同様に他のコンポーネントファイルも作成されます。この段階では中身は最小限ですが、型定義と基本構造が揃っているため、次回以降の実装がスムーズに進みます。
App.tsx も更新して、これらのコンポーネントを import する構造にしておきましょう。
> App.tsxで全コンポーネントをimportして配置して。
todosのstateは空配列で初期化して。
ハンドラー関数はまだ中身を書かなくていい。
// src/App.tsx
import { useState } from 'react';
import { Todo, FilterType } from './types/todo';
import AddTodoForm from './components/AddTodoForm';
import FilterButtons from './components/FilterButtons';
import TodoList from './components/TodoList';
import './App.css';
function App() {
const [todos, setTodos] = useState<Todo[]>([]);
const [filter, setFilter] = useState<FilterType>('all');
return (
<div className="app">
<h1>TODOアプリ</h1>
<AddTodoForm onAdd={() => {}} />
<FilterButtons
currentFilter={filter}
onFilterChange={setFilter}
counts={{ all: 0, active: 0, completed: 0 }}
/>
<TodoList
todos={todos}
onToggle={() => {}}
onDelete={() => {}}
onEdit={() => {}}
/>
</div>
);
}
export default App;
演習問題
- コンポーネント分析: 自分が普段使っている Web アプリ(Twitter、メモアプリなど)を一つ選び、コンポーネントツリーを描いてみましょう。
- 型定義の拡張:
Todo型にpriority('high' | 'medium' | 'low')フィールドを追加する場合、どのコンポーネントの Props に影響するか考えてみましょう。 - Claude Code 活用: Claude Code に「TodoItem に優先度表示を追加する場合の型定義の変更を提案して」と依頼してみましょう。
- 設計の比較: state を App ではなく TodoList に持たせた場合、どのような問題が起きるか考えてみましょう。FilterButtons からフィルターを変更できるか検討してください。
参考リンク
Lecture 3TODO追加機能 — フォームとstate管理
12:00
TODO追加機能 — フォームとstate管理
前回の講義でコンポーネント設計が完了しました。今回はいよいよ実装に入ります。TODO アプリの最も基本的な機能である「TODO の追加」を実装しましょう。React の useState フックを使ったフォーム入力の管理と、新しい TODO をリストに追加するロジックを、Claude Code と一緒に構築していきます。
useState で TODO リストを管理する
React でデータを管理するには useState フックを使います。まず、App コンポーネントに TODO リストの state を実装しましょう。
> App.tsxのtodos stateに、TODOを追加する関数addTodoを実装して。
idはcrypto.randomUUID()で生成して。
createdAtはDate.now()で。
追加したらコンソールに出力して確認できるようにして。
Claude Code が生成するコードは以下のとおりです。
// src/App.tsx
import { useState } from 'react';
import { Todo, FilterType } from './types/todo';
import AddTodoForm from './components/AddTodoForm';
import TodoList from './components/TodoList';
import './App.css';
function App() {
const [todos, setTodos] = useState<Todo[]>([]);
const [filter, setFilter] = useState<FilterType>('all');
const addTodo = (text: string) => {
const newTodo: Todo = {
id: crypto.randomUUID(),
text,
completed: false,
createdAt: Date.now(),
};
setTodos((prevTodos) => [newTodo, ...prevTodos]);
console.log('Todo added:', newTodo);
};
return (
<div className="app">
<h1>TODOアプリ</h1>
<AddTodoForm onAdd={addTodo} />
<TodoList
todos={todos}
onToggle={() => {}}
onDelete={() => {}}
onEdit={() => {}}
/>
</div>
);
}
export default App;
ここで重要なポイントが2つあります。
1つ目は setTodos の呼び出し方です。setTodos([newTodo, ...todos]) と書くこともできますが、setTodos((prevTodos) => [newTodo, ...prevTodos]) のように関数形式を使うほうが安全です。これは「関数型アップデート」と呼ばれ、前の state を確実に参照できます。
2つ目は [newTodo, ...prevTodos] の順序です。新しい TODO を先頭に追加することで、最新の TODO が画面の上に表示されます。末尾に追加したい場合は [...prevTodos, newTodo] とします。
AddTodoForm コンポーネントの実装
次に、ユーザーが TODO を入力するためのフォームコンポーネントを実装します。
> AddTodoForm.tsxを実装して。
テキスト入力欄と追加ボタンを持つフォーム。
空文字の場合は追加できないようにバリデーションして。
追加後は入力欄をクリアして。
Enterキーでも追加できるようにして。
Claude Code が生成するコードを見ていきましょう。
// src/components/AddTodoForm.tsx
import { useState, FormEvent } from 'react';
interface AddTodoFormProps {
onAdd: (text: string) => void;
}
function AddTodoForm({ onAdd }: AddTodoFormProps) {
const [inputValue, setInputValue] = useState('');
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const trimmed = inputValue.trim();
if (!trimmed) return;
onAdd(trimmed);
setInputValue('');
};
return (
<form className="add-todo-form" onSubmit={handleSubmit}>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="新しいTODOを入力..."
className="todo-input"
autoFocus
/>
<button
type="submit"
className="add-button"
disabled={!inputValue.trim()}
>
追加
</button>
</form>
);
}
export default AddTodoForm;
このコンポーネントのポイントを解説します。
制御コンポーネント(Controlled Component): input の value を useState で管理しています。React が入力値を完全に制御するため「制御コンポーネント」と呼ばれます。onChange イベントで値を更新し、value で表示をバインドします。
e.preventDefault(): フォーム送信時のデフォルト動作(ページリロード)を防止します。React の SPA ではページ遷移なしで動作するため、必ずこの処理が必要です。
trim() によるバリデーション: 空白だけの入力を防ぎます。inputValue.trim() が空文字列の場合は何もせず return します。ボタンの disabled 属性にも同じ条件を使い、視覚的にも追加できないことを示しています。
フォームのスタイルを追加する
フォームの見た目を整えましょう。Claude Code に CSS を書いてもらいます。
> AddTodoFormのスタイルをApp.cssに追加して。
入力欄とボタンを横並びにして。
モダンで見やすいデザインにして。
ボタンはdisabled時にグレーアウトして。
Claude Code が追加する CSS は以下のとおりです。
/* AddTodoForm */
.add-todo-form {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.todo-input {
flex: 1;
padding: 0.75rem 1rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
outline: none;
transition: border-color 0.2s;
}
.todo-input:focus {
border-color: #4a90d9;
}
.add-button {
padding: 0.75rem 1.5rem;
background-color: #4a90d9;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s;
}
.add-button:hover:not(:disabled) {
background-color: #357abd;
}
.add-button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
flex: 1 を入力欄に指定することで、残りのスペースを自動的に埋めます。ボタンは固定幅で、入力欄が伸縮する設計です。:focus 擬似クラスで入力中のハイライトを表示し、:disabled でボタンのグレーアウトを実現しています。
動作確認とデバッグ
ここまでの実装を動作確認しましょう。開発サーバーが起動していない場合は Claude Code で起動します。
> 開発サーバーを起動して動作確認したい
ブラウザで以下を確認してください。
- 入力欄にテキストを入力すると、追加ボタンが活性化する
- Enter キーまたは追加ボタンで TODO が追加される
- 追加後に入力欄がクリアされる
- 空文字では追加ボタンが押せない
- ブラウザの開発者ツール(F12)のコンソールに追加した TODO が表示される
もしエラーが出た場合は、Claude Code にエラーメッセージを貼り付けて相談しましょう。
> このエラーを修正して:
[エラーメッセージをここに貼り付け]
Claude Code はエラーメッセージを解析して、修正コードを提案してくれます。TypeScript の型エラーが最も多い初学者のつまずきポイントですが、Claude Code はその場で正確な型を提案してくれるので安心です。
この時点では、追加した TODO はまだ画面に表示されません。次の講義でリスト表示を実装します。コンソールに TODO オブジェクトが出力されていれば、追加ロジックは正しく動作しています。
演習問題
- 入力制限: Claude Code に「入力文字数を100文字以内に制限するバリデーションを追加して」と依頼してみましょう。残り文字数も表示してみてください。
- 初期データ:
useStateの初期値にダミーの TODO を3件入れて、state が正しく機能していることを確認しましょう。 - デバッグ: ブラウザの開発者ツールの React DevTools をインストールし、state の変化をリアルタイムで確認してみましょう。
- 機能追加: Claude Code に「入力欄にフォーカスが戻るように、追加後に自動フォーカスする機能を追加して」と依頼してみましょう。
useRefフックの使い方を学べます。
参考リンク
Lecture 4TODO一覧表示 — リストレンダリングとkey
12:00
TODO一覧表示 — リストレンダリングとkey
前回の講義で TODO の追加機能を実装しました。しかし、追加した TODO はまだ画面に表示されていません。今回は TodoList コンポーネントと TodoItem コンポーネントを実装して、TODO の一覧を画面に表示する機能を作ります。React のリストレンダリングで重要な key プロパティについても詳しく学びましょう。
TodoItem コンポーネントの実装
まず、個々の TODO アイテムを表示する TodoItem コンポーネントを実装します。Claude Code に依頼しましょう。
> TodoItem.tsxを実装して。
チェックボックス、TODOテキスト、削除ボタンを横並びで表示。
完了済みのTODOはテキストに取り消し線を付けて。
チェックボックスをクリックしたらonToggleを呼ぶ。
削除ボタンをクリックしたらonDeleteを呼ぶ。
Claude Code が生成するコードは以下のとおりです。
// src/components/TodoItem.tsx
import { Todo } from '../types/todo';
interface TodoItemProps {
todo: Todo;
onToggle: (id: string) => void;
onDelete: (id: string) => void;
onEdit: (id: string, newText: string) => void;
}
function TodoItem({ todo, onToggle, onDelete }: TodoItemProps) {
return (
<li className={`todo-item ${todo.completed ? 'completed' : ''}`}>
<label className="todo-label">
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
className="todo-checkbox"
/>
<span className="todo-text">{todo.text}</span>
</label>
<div className="todo-actions">
<button
onClick={() => onDelete(todo.id)}
className="delete-button"
aria-label="削除"
>
✕
</button>
</div>
</li>
);
}
export default TodoItem;
このコンポーネントの設計ポイントを解説します。
label 要素によるアクセシビリティ: チェックボックスと TODO テキストを label で囲むことで、テキストをクリックしても完了/未完了を切り替えられるようになります。クリック領域が広がり、ユーザビリティが向上します。
条件付きクラス名: todo.completed ? 'completed' : '' のテンプレートリテラルで、完了状態に応じてクラス名を動的に切り替えます。CSS でこのクラスに基づいてスタイルを変更します。
aria-label の設定: 削除ボタンに aria-label="削除" を設定することで、スクリーンリーダーを使っているユーザーにもボタンの目的が伝わります。
TodoList コンポーネントの実装
次に、TodoItem を一覧表示する TodoList コンポーネントを実装します。
> TodoList.tsxを実装して。
todos配列をmapでループしてTodoItemをレンダリング。
todosが空の場合は「TODOがありません」と表示して。
keyにはtodo.idを使って。
Claude Code が生成するコードです。
// src/components/TodoList.tsx
import { Todo } from '../types/todo';
import TodoItem from './TodoItem';
interface TodoListProps {
todos: Todo[];
onToggle: (id: string) => void;
onDelete: (id: string) => void;
onEdit: (id: string, newText: string) => void;
}
function TodoList({ todos, onToggle, onDelete, onEdit }: TodoListProps) {
if (todos.length === 0) {
return (
<div className="empty-message">
<p>TODOがありません。上のフォームから追加しましょう!</p>
</div>
);
}
return (
<ul className="todo-list">
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onDelete={onDelete}
onEdit={onEdit}
/>
))}
</ul>
);
}
export default TodoList;
key プロパティの重要性
リストレンダリングで最も重要な概念が key プロパティです。Claude Code に詳しく聞いてみましょう。
> ReactのkeyプロパティがなぜZz重要なのか、具体例を使って説明して。
indexをkeyに使うとどんな問題が起きるかも説明して。
key は React がリスト内のどの要素が変更・追加・削除されたかを識別するために使う特別なプロパティです。
key がない場合やインデックスを key にした場合の問題:
// 悪い例: インデックスをkeyに使う
{todos.map((todo, index) => (
<TodoItem key={index} todo={todo} ... />
))}
インデックスを key にすると、要素の追加・削除・並び替え時に React が要素を正しく追跡できません。例えば、リストの先頭に新しい TODO を追加した場合、すべてのインデックスがずれます。React は「key=0 の要素が変更された」と認識し、実際には変わっていない要素まで再レンダリングしてしまいます。
// 良い例: 一意なIDをkeyに使う
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} ... />
))}
crypto.randomUUID() で生成した一意な ID を key にすれば、React は各要素を正確に追跡できます。追加・削除時にも不要な再レンダリングが発生しません。
key の基本ルールをまとめると以下のとおりです。
- 一意であること: 兄弟要素間で重複しない値を使う
- 安定していること: レンダリングのたびに変わらない値を使う(
Math.random()は NG) - 配列のインデックスは避ける: 要素の追加・削除・並び替えがある場合は不適切
リスト表示のスタイリング
一覧表示のスタイルを追加しましょう。
> TodoListとTodoItemのCSSをApp.cssに追加して。
リストスタイルを消して、カード風にして。
完了済みアイテムはグレーアウトと取り消し線で。
削除ボタンはホバー時に赤くなるようにして。
Claude Code が生成する CSS です。
/* TodoList */
.todo-list {
list-style: none;
padding: 0;
margin: 0;
}
.empty-message {
text-align: center;
color: #999;
padding: 2rem;
font-size: 0.95rem;
}
/* TodoItem */
.todo-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
margin-bottom: 0.5rem;
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 8px;
transition: box-shadow 0.2s;
}
.todo-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.todo-item.completed {
opacity: 0.6;
background: #f9f9f9;
}
.todo-label {
display: flex;
align-items: center;
gap: 0.75rem;
flex: 1;
cursor: pointer;
}
.todo-checkbox {
width: 1.2rem;
height: 1.2rem;
cursor: pointer;
accent-color: #4a90d9;
}
.todo-text {
font-size: 1rem;
color: #333;
}
.completed .todo-text {
text-decoration: line-through;
color: #999;
}
.todo-actions {
display: flex;
gap: 0.5rem;
}
.delete-button {
background: none;
border: none;
color: #ccc;
font-size: 1.1rem;
cursor: pointer;
padding: 0.25rem 0.5rem;
border-radius: 4px;
transition: color 0.2s, background-color 0.2s;
}
.delete-button:hover {
color: #e74c3c;
background-color: #fde8e8;
}
動作確認
ブラウザで以下の動作を確認しましょう。
- TODO がない状態で「TODOがありません」が表示されること
- フォームから TODO を追加すると、リストに表示されること
- 複数の TODO を追加して、すべて正しく表示されること
- 完了チェックボックスをクリックすると取り消し線とグレーアウトが適用されること(完了・削除ロジックは次回実装しますが、UI の変化は確認できます)
まだ完了切替と削除のハンドラーが空(() => {})なので、チェックボックスのクリックや削除ボタンは動作しません。次の講義でこれらの機能を完成させます。
演習問題
- key の実験: 一時的に
key={todo.id}をkey={Math.random()}に変更し、TODO 追加時にどのような不具合が起きるか観察してみましょう。入力フォームのフォーカスが外れるなどの問題が見られるはずです。変更後は必ず元に戻してください。 - 空リスト表示: Claude Code に「空の場合に表示するイラスト(SVG)をEmptyMessageコンポーネントとして作って」と依頼してみましょう。
- 件数表示: Claude Code に「TODO リストの上に『全 X 件』と件数を表示する機能を追加して」と依頼してみましょう。
- 作成日表示: TodoItem に
createdAtをフォーマットして表示する機能を追加してみましょう。プロンプト例:TodoItemにcreatedAtを「2026/02/16 15:30」の形式で表示して
参考リンク
Lecture 5完了・削除機能 — イベント処理とstate更新
12:00
完了・削除機能 — イベント処理とstate更新
ここまでで TODO の追加と一覧表示ができるようになりました。今回は、TODO の完了切替と削除機能を実装します。React で state を更新する際に最も重要な「イミュータブル(不変)な更新パターン」を理解し、Claude Code を使って正しい実装を行いましょう。Module 1 の最終回として、基本的な CRUD 操作の大部分を完成させます。
完了切替機能の実装
TODO の完了状態を切り替える toggleTodo 関数を App コンポーネントに実装します。
> App.tsxにtoggleTodo関数を実装して。
指定されたidのTODOのcompletedを反転させる。
イミュータブルな更新パターンを使って。
TodoListのonToggleにこの関数を渡して。
Claude Code が生成するコードです。
const toggleTodo = (id: string) => {
setTodos((prevTodos) =>
prevTodos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
このコードで最も重要なのは「イミュータブル(不変)な更新」のパターンです。React の state は直接変更してはいけません。なぜなら、React は state オブジェクトの参照が変わったかどうかで再レンダリングの要否を判断するからです。
やってはいけない例(ミュータブルな更新):
// NG: 配列を直接変更している
const toggleTodoBad = (id: string) => {
const todo = todos.find((t) => t.id === id);
if (todo) {
todo.completed = !todo.completed; // 直接変更 = NG
setTodos(todos); // 同じ参照なのでReactが変化を検知しない
}
};
このコードでは、配列の中のオブジェクトを直接変更しています。setTodos(todos) を呼んでも、配列の参照が変わっていないため、React は変更を検知できず、画面が更新されません。
正しい例(イミュータブルな更新):
// OK: 新しい配列と新しいオブジェクトを作成
const toggleTodo = (id: string) => {
setTodos((prevTodos) =>
prevTodos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
map() は新しい配列を返し、{ ...todo, completed: !todo.completed } はスプレッド構文で新しいオブジェクトを作成しています。元の配列もオブジェクトも一切変更されていません。これがイミュータブルな更新パターンです。
削除機能の実装
続いて、TODO を削除する deleteTodo 関数を実装しましょう。
> App.tsxにdeleteTodo関数を実装して。
指定されたidのTODOを配列から取り除く。
filterメソッドを使ってイミュータブルに。
確認ダイアログは不要。
Claude Code が生成するコードです。
const deleteTodo = (id: string) => {
setTodos((prevTodos) => prevTodos.filter((todo) => todo.id !== id));
};
filter() メソッドは条件に一致する要素だけを含む新しい配列を返します。todo.id !== id が true の要素、つまり削除対象以外の TODO だけが残ります。filter() は元の配列を変更しないため、イミュータブルな更新パターンに沿っています。
App.tsx の完成形
ここまでの実装をまとめた App.tsx の全体像を確認しましょう。
> App.tsxの全体コードを見せて。
addTodo、toggleTodo、deleteTodoが全て実装された状態で。
// src/App.tsx
import { useState } from 'react';
import { Todo, FilterType } from './types/todo';
import AddTodoForm from './components/AddTodoForm';
import TodoList from './components/TodoList';
import './App.css';
function App() {
const [todos, setTodos] = useState<Todo[]>([]);
const [filter, setFilter] = useState<FilterType>('all');
const addTodo = (text: string) => {
const newTodo: Todo = {
id: crypto.randomUUID(),
text,
completed: false,
createdAt: Date.now(),
};
setTodos((prevTodos) => [newTodo, ...prevTodos]);
};
const toggleTodo = (id: string) => {
setTodos((prevTodos) =>
prevTodos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const deleteTodo = (id: string) => {
setTodos((prevTodos) => prevTodos.filter((todo) => todo.id !== id));
};
return (
<div className="app">
<h1>TODOアプリ</h1>
<AddTodoForm onAdd={addTodo} />
<TodoList
todos={todos}
onToggle={toggleTodo}
onDelete={deleteTodo}
onEdit={() => {}}
/>
</div>
);
}
export default App;
3つの state 更新関数に注目してください。
| 関数 | 使用メソッド | 操作 |
|---|---|---|
addTodo |
スプレッド構文 | 先頭に追加 |
toggleTodo |
map() |
特定要素を変更 |
deleteTodo |
filter() |
特定要素を除外 |
これらはすべて新しい配列を生成する非破壊的メソッドです。React の state 更新では push、splice、sort(インプレース)などの破壊的メソッドは使ってはいけません。代わりに map、filter、スプレッド構文、concat、slice、toSorted などの非破壊的メソッドを使います。
イベント処理の仕組み
React のイベント処理について、もう少し深く理解しましょう。Claude Code に解説を聞きます。
> Reactのイベント処理で、onClickにアロー関数を使う場合と
関数参照を直接渡す場合の違いを説明して。
TodoItemのコードで具体的に示して。
TodoItem での2つのパターンを比較します。
// パターン1: アロー関数でラップ(引数あり)
<button onClick={() => onDelete(todo.id)}>削除</button>
// パターン2: 関数参照を直接渡す(引数なし)
<button onClick={handleClick}>削除</button>
パターン1は todo.id という引数を渡す必要があるため、アロー関数でラップしています。クリック時に () => onDelete(todo.id) が実行され、onDelete に todo.id が渡されます。
もし onClick={onDelete(todo.id)} と書いてしまうと、レンダリング時に onDelete が即座に実行されてしまいます。onClick にはイベントハンドラー(関数)を渡す必要があるため、アロー関数でラップするか、bind を使います。
動作確認とデバッグ
完成した機能を確認しましょう。ブラウザで以下のテストを実行してください。
- TODO を3件追加する(「買い物」「掃除」「勉強」)
- 「買い物」のチェックボックスをクリック → 取り消し線が表示される
- もう一度クリック → 取り消し線が消える
- 「掃除」の削除ボタンをクリック → リストから消える
- 残りの TODO が正しく表示されていることを確認
問題が起きた場合は Claude Code にデバッグを依頼しましょう。
> チェックボックスをクリックしても画面が更新されない。
原因を調べて修正して。
演習問題
- 一括操作: Claude Code に「すべてのTODOを完了にするボタンと、完了済みを一括削除するボタンを追加して」と依頼してみましょう。
- 元に戻す機能: 削除した TODO を1件だけ元に戻せる「Undo」ボタンを実装してみましょう。プロンプト例:
最後に削除したTODOを復元できるUndoボタンを追加して。useStateで直前の削除を保持して。 - 並び替え: Claude Code に「TODO を作成日時の新しい順・古い順で並び替えるボタンを追加して」と依頼してみましょう。
toSorted()メソッドを使うイミュータブルなパターンを学べます。 - TypeScript の恩恵:
toggleTodoの引数の型をnumberに変更してみて、TypeScript がどのようにエラーを表示するか確認しましょう。型安全性のメリットを実感できます。
参考リンク
Lecture 6localStorage永続化 — データを保存する
12:00
localStorage永続化 — データを保存する
Module 1 で TODO の追加・表示・完了・削除ができるようになりました。しかし、ブラウザをリロードすると TODO はすべて消えてしまいます。Module 2 の最初の講義では、ブラウザの localStorage を使ってデータを永続化する方法を学びます。React の useEffect フックを使って、state の変化を自動的にローカルストレージに同期させます。
localStorage の基本
localStorage はブラウザに組み込まれたキーバリューストアです。データは文字列として保存され、ブラウザを閉じても永続化されます。まず基本的な API を理解しましょう。
> localStorageの基本的なAPIを説明して。
setItem、getItem、removeItem、clearの使い方をそれぞれ例示して。
// データの保存
localStorage.setItem('key', 'value');
// データの取得
const value = localStorage.getItem('key'); // 'value' or null
// データの削除
localStorage.removeItem('key');
// 全データの削除
localStorage.clear();
localStorage は文字列しか保存できないため、オブジェクトや配列を保存するには JSON.stringify() でJSON文字列に変換し、取得時に JSON.parse() でオブジェクトに戻す必要があります。
// 配列の保存
const todos = [{ id: '1', text: '買い物', completed: false }];
localStorage.setItem('todos', JSON.stringify(todos));
// 配列の取得
const stored = localStorage.getItem('todos');
const parsed = stored ? JSON.parse(stored) : [];
getItem はキーが存在しない場合に null を返すため、必ず null チェックが必要です。これを忘れると JSON.parse(null) でエラーが発生します。
useEffect で state を localStorage に同期する
useEffect は副作用(side effect)を扱うための React フックです。state が変更されたときに localStorage にデータを保存する処理は、まさに副作用にあたります。Claude Code に実装を依頼しましょう。
> App.tsxにuseEffectを使って、todosが変更されるたびに
localStorageに保存する処理を追加して。
キー名は "todos" で。
Claude Code が生成するコードです。
import { useState, useEffect } from 'react';
function App() {
const [todos, setTodos] = useState<Todo[]>([]);
const [filter, setFilter] = useState<FilterType>('all');
// todosが変更されるたびにlocalStorageに保存
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
// ... 以下省略
}
useEffect の第2引数 [todos] は「依存配列(dependency array)」と呼ばれます。この配列に含まれる値が変化したときだけ、エフェクトが実行されます。
| 依存配列 | 実行タイミング |
|---|---|
[todos] |
todos が変更されたとき |
[] |
初回レンダリング時のみ |
| 省略 | 毎回のレンダリング時 |
今回は [todos] を指定しているので、addTodo、toggleTodo、deleteTodo のいずれかで todos が更新されるたびに自動的に localStorage に保存されます。
初期データの読み込み
保存だけでは不十分です。ページ読み込み時に localStorage からデータを復元する必要があります。
> useState の初期値をlocalStorageから読み込むように変更して。
localStorageにデータがない場合は空配列にして。
JSON.parseのエラーハンドリングも入れて。
Claude Code が生成するコードです。
function App() {
const [todos, setTodos] = useState<Todo[]>(() => {
try {
const stored = localStorage.getItem('todos');
if (stored) {
return JSON.parse(stored) as Todo[];
}
} catch (error) {
console.error('Failed to load todos from localStorage:', error);
}
return [];
});
// ... 以下同じ
}
ここで重要なのは、useState の初期値に 関数(遅延初期化) を渡していることです。
// 毎レンダリング時に実行される(パフォーマンス悪)
const [todos, setTodos] = useState(JSON.parse(localStorage.getItem('todos') || '[]'));
// 初回レンダリング時のみ実行される(推奨)
const [todos, setTodos] = useState(() => {
return JSON.parse(localStorage.getItem('todos') || '[]');
});
関数を渡す形式(遅延初期化)では、localStorage の読み込みと JSON パースが初回レンダリング時にのみ実行されます。通常の値として渡すと、毎回のレンダリング時にこの計算が実行されてしまい、パフォーマンスに影響します。
try-catch で JSON.parse のエラーをキャッチしているのも重要なポイントです。localStorage のデータが破損していたり、別のアプリケーションが同じキーに不正なデータを書き込んでいた場合に、アプリがクラッシュするのを防ぎます。
カスタムフックへの抽出
localStorage との同期ロジックを再利用可能なカスタムフックに抽出しましょう。Claude Code に依頼します。
> localStorageとuseStateを統合するカスタムフック useLocalStorage を作成して。
src/hooks/useLocalStorage.ts に配置して。
ジェネリクスで型安全にして。
Claude Code が生成するカスタムフックです。
// src/hooks/useLocalStorage.ts
import { useState, useEffect } from 'react';
function useLocalStorage<T>(key: string, initialValue: T): [T, React.Dispatch<React.SetStateAction<T>>] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.error(`Error writing localStorage key "${key}":`, error);
}
}, [key, storedValue]);
return [storedValue, setStoredValue];
}
export default useLocalStorage;
このカスタムフックを使えば、App コンポーネントのコードがシンプルになります。
// Before
const [todos, setTodos] = useState<Todo[]>([]);
// After
const [todos, setTodos] = useLocalStorage<Todo[]>('todos', []);
useLocalStorage はジェネリクス <T> を使っているため、どんな型のデータでも型安全に保存・復元できます。useState と同じインターフェースを持つので、差し替えるだけで永続化が完了します。
動作確認
以下の手順で永続化が正しく動作しているか確認しましょう。
- TODO を数件追加する
- ブラウザの開発者ツール(F12)→ Application → Local Storage で
todosキーのデータを確認 - ブラウザをリロード(F5)して、TODO が保持されていることを確認
- TODO を完了・削除して、リロード後もその状態が保持されることを確認
- 開発者ツールから localStorage のデータを手動で削除し、リロード後に空のリストが表示されることを確認
演習問題
- フィルター状態の保存:
filterの state もuseLocalStorageを使って永続化してみましょう。リロード後も同じフィルターが選択されているようにしてください。 - データ容量の確認: Claude Code に「現在のlocalStorageの使用量を表示するコンポーネントを作って」と依頼してみましょう。localStorage の容量制限(通常5MB)についても調べてみてください。
- エクスポート機能: Claude Code に「TODOデータをJSONファイルとしてダウンロードするボタンを追加して」と依頼してみましょう。
BlobとURL.createObjectURLを使ったファイルダウンロードの実装を学べます。 - useEffect の理解:
useEffectの依存配列を空[]にした場合や省略した場合の動作の違いを、Claude Code に具体的に説明してもらい、実際にコードを変更して動作を確認してみましょう。
参考リンク
Lecture 7フィルタリング — 表示切替機能を作る
12:00
フィルタリング — 表示切替機能を作る
TODO が増えてくると、すべての項目を一度に見るのは大変です。この講義では、「すべて」「未完了」「完了済み」の3つのフィルターを使って表示を切り替える機能を実装します。React の useMemo フックを使ったパフォーマンス最適化と、FilterButtons コンポーネントの設計についても学びます。
フィルタリングのロジック
まず、App コンポーネントにフィルタリングのロジックを追加しましょう。Claude Code に依頼します。
> App.tsxにフィルタリング機能を追加して。
filter stateに応じてtodosをフィルタリングして、
filteredTodosをTodoListに渡す。
useMemoを使ってパフォーマンス最適化して。
Claude Code が生成するコードです。
import { useState, useMemo } from 'react';
function App() {
const [todos, setTodos] = useLocalStorage<Todo[]>('todos', []);
const [filter, setFilter] = useState<FilterType>('all');
const filteredTodos = useMemo(() => {
switch (filter) {
case 'active':
return todos.filter((todo) => !todo.completed);
case 'completed':
return todos.filter((todo) => todo.completed);
case 'all':
default:
return todos;
}
}, [todos, filter]);
const counts = useMemo(() => ({
all: todos.length,
active: todos.filter((todo) => !todo.completed).length,
completed: todos.filter((todo) => todo.completed).length,
}), [todos]);
return (
<div className="app">
<h1>TODOアプリ</h1>
<AddTodoForm onAdd={addTodo} />
<FilterButtons
currentFilter={filter}
onFilterChange={setFilter}
counts={counts}
/>
<TodoList
todos={filteredTodos}
onToggle={toggleTodo}
onDelete={deleteTodo}
onEdit={() => {}}
/>
</div>
);
}
useMemo によるパフォーマンス最適化
useMemo は計算結果をキャッシュ(メモ化)するフックです。依存する値が変更されたときだけ再計算されます。
const filteredTodos = useMemo(() => {
// この関数は todos または filter が変更されたときだけ実行される
switch (filter) {
case 'active':
return todos.filter((todo) => !todo.completed);
case 'completed':
return todos.filter((todo) => todo.completed);
default:
return todos;
}
}, [todos, filter]); // 依存配列
useMemo を使わない場合、コンポーネントが再レンダリングされるたびにフィルタリング処理が実行されます。TODO が数件の場合は問題ありませんが、大量のデータを扱う場合にパフォーマンスに影響する可能性があります。
ただし、useMemo を過剰に使うとかえってコードが複雑になります。React 公式ドキュメントでも「本当にパフォーマンス問題がある場合にのみ使用すること」が推奨されています。今回は学習目的で導入しますが、TODO アプリ程度のデータ量では useMemo なしでも問題ありません。
counts もメモ化しています。フィルターボタンに表示する各カテゴリの件数は、todos が変更されたときだけ再計算すれば十分です。
const counts = useMemo(() => ({
all: todos.length,
active: todos.filter((todo) => !todo.completed).length,
completed: todos.filter((todo) => todo.completed).length,
}), [todos]);
FilterButtons コンポーネントの実装
フィルター切替の UI を実装します。Claude Code に依頼しましょう。
> FilterButtons.tsxを実装して。
「すべて」「未完了」「完了済み」の3つのボタン。
現在選択中のフィルターはアクティブなスタイルにして。
各ボタンに件数バッジを表示して。
Claude Code が生成するコードです。
// src/components/FilterButtons.tsx
import { FilterType } from '../types/todo';
interface FilterButtonsProps {
currentFilter: FilterType;
onFilterChange: (filter: FilterType) => void;
counts: {
all: number;
active: number;
completed: number;
};
}
const filterLabels: Record<FilterType, string> = {
all: 'すべて',
active: '未完了',
completed: '完了済み',
};
function FilterButtons({ currentFilter, onFilterChange, counts }: FilterButtonsProps) {
const filters: FilterType[] = ['all', 'active', 'completed'];
return (
<div className="filter-buttons">
{filters.map((filter) => (
<button
key={filter}
className={`filter-button ${currentFilter === filter ? 'active' : ''}`}
onClick={() => onFilterChange(filter)}
>
{filterLabels[filter]}
<span className="filter-count">{counts[filter]}</span>
</button>
))}
</div>
);
}
export default FilterButtons;
このコンポーネントでは、いくつかの設計テクニックを使っています。
Record<FilterType, string> 型: filterLabels に使っている Record 型は、キーが FilterType の値で、値が string のオブジェクト型を定義します。フィルター種別とラベル文字列のマッピングを型安全に管理できます。新しいフィルター種別を FilterType に追加すれば、filterLabels にもエントリを追加しないと型エラーになるため、追加漏れを防げます。
配列から動的にボタンを生成: filters 配列を map でループしてボタンを生成しています。新しいフィルターを追加する場合は配列に要素を追加するだけで済みます。
フィルターボタンのスタイリング
フィルターボタンの CSS を追加しましょう。
> FilterButtonsのCSSをApp.cssに追加して。
ボタンを横並びにして、アクティブなボタンは青い背景にして。
件数バッジを小さく表示して。
ボタン間に適切な間隔を設けて。
Claude Code が生成する CSS です。
/* FilterButtons */
.filter-buttons {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
justify-content: center;
}
.filter-button {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 1rem;
border: 2px solid #e0e0e0;
border-radius: 20px;
background: #fff;
color: #666;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s;
}
.filter-button:hover {
border-color: #4a90d9;
color: #4a90d9;
}
.filter-button.active {
background: #4a90d9;
border-color: #4a90d9;
color: #fff;
}
.filter-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.4rem;
height: 1.4rem;
padding: 0 0.3rem;
border-radius: 10px;
font-size: 0.75rem;
font-weight: bold;
background: rgba(0, 0, 0, 0.1);
}
.filter-button.active .filter-count {
background: rgba(255, 255, 255, 0.3);
}
ボタンは border-radius: 20px でピル型にし、アクティブ状態では青い背景に白文字で表示しています。件数バッジは小さな丸いインジケーターとして、ボタン内の右側に表示されます。
フィルタリングと空の状態の連携
フィルターを切り替えた結果、表示する TODO がない場合のメッセージも更新しましょう。
> TodoListの空メッセージを、現在のフィルターに応じて変えて。
「すべて」で空なら「TODOがありません」
「未完了」で空なら「未完了のTODOはありません」
「完了済み」で空なら「完了済みのTODOはありません」
TodoList にフィルター情報を渡すか、または App 側でメッセージを制御する方法があります。シンプルな方法として、TodoList の Props にオプショナルな emptyMessage を追加します。
// App.tsx での使用
const emptyMessages: Record<FilterType, string> = {
all: 'TODOがありません。上のフォームから追加しましょう!',
active: '未完了のTODOはありません。すべて完了です!',
completed: '完了済みのTODOはありません。',
};
<TodoList
todos={filteredTodos}
onToggle={toggleTodo}
onDelete={deleteTodo}
onEdit={() => {}}
emptyMessage={emptyMessages[filter]}
/>
演習問題
- フィルター永続化:
filterの state もuseLocalStorageを使って永続化し、リロード後も同じフィルターが選択されるようにしてみましょう。 - カウント表示: Claude Code に「全体の完了率をパーセンテージで表示するプログレスバーを追加して」と依頼してみましょう。
useMemoを使って完了率を計算してください。 - 検索フィルター: Claude Code に「テキスト検索でTODOをフィルタリングする入力欄を追加して。フィルターボタンとの組み合わせで動作するように」と依頼してみましょう。
- useMemo の理解:
useMemoを一時的に外して、React DevTools の Profiler でレンダリング回数を比較してみましょう。小さなアプリでは差が出ないことを確認するのも学びになります。
参考リンク
Lecture 8CSSスタイリング — 見た目を整える
12:00
CSSスタイリング — 見た目を整える
TODO アプリの機能はほぼ完成しました。この講義では、アプリ全体の見た目を本格的に仕上げます。CSS の設計パターンを学びながら、レスポンシブデザイン、ダークモード対応、アニメーションなどを Claude Code と一緒に実装していきましょう。プロフェッショナルな仕上がりの TODO アプリを目指します。
全体のレイアウトとベーススタイル
まず、アプリ全体の基盤となるスタイルを整えましょう。Claude Code に依頼します。
> App.cssを全面的にリニューアルして。
- ページ全体の背景をグラデーションにして
- アプリ本体をカードスタイルにして
- モダンでクリーンなデザインにして
- フォントはシステムフォントを使って
Claude Code が生成する CSS の全体設計です。
/* ベーススタイル */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Hiragino Sans',
'Noto Sans JP', sans-serif;
display: flex;
justify-content: center;
padding: 2rem 1rem;
}
#root {
width: 100%;
max-width: 600px;
}
/* アプリコンテナ */
.app {
background: #fff;
border-radius: 16px;
padding: 2rem;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
}
/* タイトル */
.app h1 {
text-align: center;
color: #333;
font-size: 1.8rem;
margin-bottom: 1.5rem;
font-weight: 700;
}
box-sizing: border-box をすべての要素に適用することで、padding や border がボックスサイズに含まれるようにしています。これにより、レイアウト計算が直感的になります。
背景にグラデーションを使い、アプリ本体を白いカードとして浮かせるデザインは、モダンなWebアプリでよく見られるパターンです。box-shadow で奥行きを表現し、border-radius: 16px で角を丸めることでソフトな印象を与えます。
フォームのスタイル改善
入力フォームをさらに洗練させましょう。
> AddTodoFormのスタイルを改善して。
- 入力欄に影をつけてフォーカス時に強調
- ボタンにグラデーションを使って
- スムーズなトランジションを入れて
Claude Code が生成するフォームの CSS です。
/* AddTodoForm */
.add-todo-form {
display: flex;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.todo-input {
flex: 1;
padding: 0.875rem 1.25rem;
border: 2px solid #e8e8e8;
border-radius: 12px;
font-size: 1rem;
outline: none;
transition: all 0.3s ease;
background: #f8f9fa;
}
.todo-input:focus {
border-color: #667eea;
background: #fff;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15);
}
.todo-input::placeholder {
color: #aab;
}
.add-button {
padding: 0.875rem 1.75rem;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 12px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
}
.add-button:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
.add-button:active:not(:disabled) {
transform: translateY(0);
}
.add-button:disabled {
background: #ddd;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
:focus 時の box-shadow で、入力欄の周囲にソフトなグロー効果を表示しています。0 0 0 3px は影のオフセットなし・ぼかしなし・広がり3pxの設定で、border のように見えますが実際は影です。この技法は outline の代わりとして広く使われています。
ボタンの :hover で transform: translateY(-1px) を使い、マウスオーバー時にわずかに浮き上がるエフェクトを実現しています。box-shadow と組み合わせることで、立体的なクリック感を演出します。
TODO アイテムのアニメーション
TODO アイテムの追加・削除時にアニメーションを付けると、ユーザー体験が大幅に向上します。
> TodoItemにCSSアニメーションを追加して。
- 追加時にフェードイン + スライドダウン
- ホバー時に軽い浮き上がり効果
- 完了時のスムーズなスタイル変化
Claude Code が生成するアニメーション CSS です。
/* TodoItem アニメーション */
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.todo-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.875rem 1rem;
margin-bottom: 0.5rem;
background: #fff;
border: 1px solid #eee;
border-radius: 12px;
transition: all 0.3s ease;
animation: slideIn 0.3s ease;
}
.todo-item:hover {
transform: translateX(4px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
border-color: #ddd;
}
.todo-item.completed {
opacity: 0.55;
background: #f9f9fa;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
color: #aaa;
}
/* チェックボックスのカスタマイズ */
.todo-checkbox {
appearance: none;
-webkit-appearance: none;
width: 1.3rem;
height: 1.3rem;
border: 2px solid #ddd;
border-radius: 50%;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
flex-shrink: 0;
}
.todo-checkbox:checked {
background: linear-gradient(135deg, #667eea, #764ba2);
border-color: transparent;
}
.todo-checkbox:checked::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 0.75rem;
font-weight: bold;
}
.todo-checkbox:hover {
border-color: #667eea;
}
appearance: none でブラウザデフォルトのチェックボックスを非表示にし、完全にカスタムデザインで置き換えています。:checked 擬似クラスでチェック時のスタイルを、::after 擬似要素でチェックマークを表示しています。円形のチェックボックスにグラデーション背景を適用することで、アプリ全体のデザインと統一感を持たせています。
レスポンシブデザイン
スマートフォンでも快適に使えるようにレスポンシブ対応を行います。
> レスポンシブデザインのCSSを追加して。
- スマホ(480px以下)でフォームを縦並びにして
- タブレットとPCで適切に表示されるように
- フォントサイズやパディングも調整して
Claude Code が生成するメディアクエリです。
/* レスポンシブ対応 */
@media (max-width: 480px) {
body {
padding: 1rem 0.5rem;
}
.app {
padding: 1.25rem;
border-radius: 12px;
}
.app h1 {
font-size: 1.4rem;
}
.add-todo-form {
flex-direction: column;
}
.add-button {
width: 100%;
text-align: center;
}
.filter-buttons {
flex-wrap: wrap;
}
.filter-button {
flex: 1;
justify-content: center;
min-width: 0;
font-size: 0.82rem;
padding: 0.45rem 0.7rem;
}
}
480px 以下のスマートフォン表示では、フォームを縦並び(flex-direction: column)に切り替え、追加ボタンを全幅に広げています。フィルターボタンも flex-wrap: wrap で折り返し可能にし、小さな画面でもすべてのボタンが操作しやすいようにしています。
レスポンシブデザインで重要なのは「モバイルファースト」の考え方です。デフォルトのスタイルをモバイル向けに書き、min-width メディアクエリで大きな画面用のスタイルを追加するのが推奨パターンです。今回は既存のスタイルをベースにしているため max-width を使っていますが、新規プロジェクトではモバイルファーストを意識しましょう。
動作確認
スタイリングの確認は以下の方法で行います。
- ブラウザの開発者ツールを開き、デバイスツールバー(レスポンシブモード)でスマートフォンサイズの表示を確認
- TODO の追加時のアニメーション(フェードイン)を確認
- チェックボックスのカスタムデザインが正しく動作しているか確認
- フィルターボタンのアクティブ状態のスタイルを確認
演習問題
- ダークモード: Claude Code に「
prefers-color-scheme: darkメディアクエリを使ってダークモードに対応して」と依頼してみましょう。 - テーマ切替: Claude Code に「ライトモードとダークモードを手動で切り替えるトグルボタンを追加して。useStateとCSS変数を使って」と依頼してみましょう。
- フォントカスタマイズ: Google Fonts から好きなフォントを選び、Claude Code に「このフォントを適用して」と依頼してみましょう。
- マイクロインタラクション: Claude Code に「削除ボタンクリック時にアイテムがフェードアウトするアニメーションを追加して」と依頼してみましょう。CSS の
@keyframesを使ったより複雑なアニメーションの実装を学べます。
参考リンク
Lecture 9編集機能 — TODOの内容を修正する
12:00
編集機能 — TODOの内容を修正する
TODO アプリに欠かせない「編集機能」を実装します。一度追加した TODO の内容を後から変更できるようにします。インライン編集(一覧表示上でそのまま編集)のパターンを採用し、編集モードの state 管理、保存・キャンセル処理、キーボードショートカットを実装していきましょう。Claude Code を使って、ユーザー体験の高い編集 UI を構築します。
編集ロジックを App コンポーネントに追加する
まず、App コンポーネントに TODO テキストを更新する関数を追加します。
> App.tsxにeditTodo関数を実装して。
引数はidとnewTextの2つ。
指定されたidのTODOのtextをnewTextに更新する。
空文字のnewTextは拒否して。
TodoListのonEditにこの関数を渡して。
Claude Code が生成するコードです。
const editTodo = (id: string, newText: string) => {
const trimmed = newText.trim();
if (!trimmed) return;
setTodos((prevTodos) =>
prevTodos.map((todo) =>
todo.id === id ? { ...todo, text: trimmed } : todo
)
);
};
// JSX内
<TodoList
todos={filteredTodos}
onToggle={toggleTodo}
onDelete={deleteTodo}
onEdit={editTodo}
/>
editTodo の実装パターンは toggleTodo とほぼ同じです。map() で全要素をイテレーションし、対象の ID に一致する要素だけスプレッド構文で新しいオブジェクトを作成して text を更新します。trim() と空文字チェックによるバリデーションも含めています。
TodoItem に編集モードを追加する
TodoItem コンポーネントにインライン編集機能を実装します。これは、TODO のテキスト表示部分をクリックすると入力欄に切り替わり、その場で編集できる仕組みです。
> TodoItem.tsxに編集機能を追加して。
- 編集ボタンをクリックすると、テキスト部分が入力欄に切り替わる
- 入力欄の横に保存ボタンとキャンセルボタンを表示
- Enterキーで保存、Escapeキーでキャンセル
- 編集開始時に入力欄にフォーカスを当てる(useRefを使って)
- 空文字で保存しようとした場合は保存しない
Claude Code が生成するコードです。
// src/components/TodoItem.tsx
import { useState, useRef, useEffect, KeyboardEvent } from 'react';
import { Todo } from '../types/todo';
interface TodoItemProps {
todo: Todo;
onToggle: (id: string) => void;
onDelete: (id: string) => void;
onEdit: (id: string, newText: string) => void;
}
function TodoItem({ todo, onToggle, onDelete, onEdit }: TodoItemProps) {
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState(todo.text);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [isEditing]);
const handleSave = () => {
const trimmed = editText.trim();
if (trimmed && trimmed !== todo.text) {
onEdit(todo.id, trimmed);
}
setIsEditing(false);
setEditText(todo.text);
};
const handleCancel = () => {
setIsEditing(false);
setEditText(todo.text);
};
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
handleSave();
} else if (e.key === 'Escape') {
handleCancel();
}
};
const startEditing = () => {
setEditText(todo.text);
setIsEditing(true);
};
return (
<li className={`todo-item ${todo.completed ? 'completed' : ''}`}>
<label className="todo-label">
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
className="todo-checkbox"
/>
{isEditing ? (
<input
ref={inputRef}
type="text"
value={editText}
onChange={(e) => setEditText(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={handleSave}
className="edit-input"
/>
) : (
<span
className="todo-text"
onDoubleClick={startEditing}
>
{todo.text}
</span>
)}
</label>
<div className="todo-actions">
{isEditing ? (
<>
<button onClick={handleSave} className="save-button" aria-label="保存">
✓
</button>
<button onClick={handleCancel} className="cancel-button" aria-label="キャンセル">
✕
</button>
</>
) : (
<>
<button onClick={startEditing} className="edit-button" aria-label="編集">
✎
</button>
<button onClick={() => onDelete(todo.id)} className="delete-button" aria-label="削除">
✕
</button>
</>
)}
</div>
</li>
);
}
export default TodoItem;
useRef によるフォーカス制御
このコンポーネントで初めて登場する useRef フックについて詳しく見ていきましょう。
> useRefの使い方と、useStateとの違いを説明して。
今回のフォーカス制御での使い方を具体的に。
useRef は React コンポーネント内で「レンダリングに影響しない参照」を保持するためのフックです。主に DOM 要素への直接アクセスに使われます。
const inputRef = useRef<HTMLInputElement>(null);
// JSXでrefを紐付ける
<input ref={inputRef} ... />
// useEffectで直接DOM操作
useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus(); // フォーカスを当てる
inputRef.current.select(); // テキストを全選択
}
}, [isEditing]);
useRef と useState の違いは以下のとおりです。
| 特性 | useState | useRef |
|---|---|---|
| 値の更新 | 再レンダリングを引き起こす | 再レンダリングを引き起こさない |
| 用途 | UI に表示するデータ | DOM 参照、タイマー ID など |
| アクセス | [value, setter] |
.current プロパティ |
今回は、編集モードに切り替わった直後に入力欄にフォーカスを当て、既存のテキストを全選択するために useRef を使っています。ユーザーがすぐに新しいテキストを入力できるように、UX を向上させる重要な処理です。
編集 UI のスタイリング
編集モードのスタイルを追加しましょう。
> 編集モードのCSSをApp.cssに追加して。
- 編集入力欄はテキスト表示と同じサイズにして
- 保存ボタンは緑、キャンセルボタンはグレーに
- 編集ボタンはホバー時に表示されるようにして
- 編集中のアイテムには視覚的な強調を付けて
Claude Code が生成する CSS です。
/* 編集モード */
.edit-input {
flex: 1;
padding: 0.25rem 0.5rem;
border: 2px solid #667eea;
border-radius: 6px;
font-size: 1rem;
outline: none;
background: #f0f4ff;
font-family: inherit;
}
.todo-item:has(.edit-input) {
border-color: #667eea;
background: #fafbff;
}
/* 編集・保存・キャンセルボタン */
.edit-button {
background: none;
border: none;
color: #ccc;
font-size: 1.1rem;
cursor: pointer;
padding: 0.25rem 0.5rem;
border-radius: 4px;
transition: all 0.2s;
opacity: 0;
}
.todo-item:hover .edit-button {
opacity: 1;
}
.edit-button:hover {
color: #667eea;
background: #f0f4ff;
}
.save-button {
background: none;
border: none;
color: #27ae60;
font-size: 1.1rem;
cursor: pointer;
padding: 0.25rem 0.5rem;
border-radius: 4px;
transition: all 0.2s;
}
.save-button:hover {
background: #e8f8f0;
}
.cancel-button {
background: none;
border: none;
color: #999;
font-size: 1.1rem;
cursor: pointer;
padding: 0.25rem 0.5rem;
border-radius: 4px;
transition: all 0.2s;
}
.cancel-button:hover {
color: #666;
background: #f0f0f0;
}
:has(.edit-input) セレクターは比較的新しい CSS 機能で、子要素に .edit-input を持つ .todo-item にスタイルを適用します。編集中のアイテム全体をハイライトして、どの TODO を編集しているか視覚的に分かりやすくしています。
編集ボタンは opacity: 0 で通常時は非表示にし、アイテムにマウスオーバーしたときだけ opacity: 1 で表示されます。これにより、UI がクリーンに保たれつつ、必要なときだけ操作ボタンが現れるインタラクションを実現しています。
ダブルクリックによる編集開始
編集ボタンに加えて、テキストのダブルクリックでも編集を開始できるようにしています。
<span
className="todo-text"
onDoubleClick={startEditing}
>
{todo.text}
</span>
onDoubleClick は React のイベントハンドラーで、要素をダブルクリックしたときに呼ばれます。多くの TODO アプリやファイルマネージャーで採用されている操作パターンで、ユーザーにとって直感的です。シングルクリックは label によるチェックボックスの切替に使われているため、ダブルクリックとの使い分けが重要です。
演習問題
- バリデーション強化: Claude Code に「編集時にも文字数制限(100文字)を追加して、残り文字数を表示して」と依頼してみましょう。
- 編集履歴: Claude Code に「TODOの編集履歴を保持して、いつ最後に編集されたかを表示する機能を追加して」と依頼してみましょう。
Todo型にupdatedAtフィールドを追加します。 - ドラッグ&ドロップ: Claude Code に「TODOの並び順をドラッグ&ドロップで変更できる機能を追加して」と依頼してみましょう。(ヒント:
react-beautiful-dndや HTML Drag and Drop API が使えます) - アクセシビリティ: Claude Code に「キーボードだけで全ての操作(追加・編集・完了・削除)ができるようにアクセシビリティを改善して」と依頼してみましょう。Tab キーでのフォーカス移動や ARIA 属性の設定を学べます。
参考リンク
Lecture 10ビルドとデプロイ — 完成アプリを公開する
12:00
ビルドとデプロイ — 完成アプリを公開する
コース最終回です。これまでの9回の講義で、Claude Code を使って TODO アプリの全機能を実装してきました。最後に、アプリをプロダクションビルドして、インターネット上に公開(デプロイ)する方法を学びます。Vercel を使えば、無料でアプリを公開でき、Git と連携した自動デプロイも設定できます。
プロダクションビルドの実行
開発サーバー(npm run dev)は開発中の利便性に最適化されていますが、本番環境には適していません。プロダクションビルドを実行して、最適化されたファイルを生成しましょう。
> プロダクションビルドを実行して。
ビルド結果のファイルサイズも確認して。
Claude Code が実行するコマンドです。
npm run build
ビルドが成功すると、dist/ ディレクトリに以下のファイルが生成されます。
dist/
├── index.html
└── assets/
├── index-[hash].js # JavaScript(ミニファイ済み)
├── index-[hash].css # CSS(ミニファイ済み)
└── vite.svg
ビルド出力の例です。
vite v6.x.x building for production...
✓ 34 modules transformed.
dist/index.html 0.46 kB │ gzip: 0.29 kB
dist/assets/index-D4jk2s.css 2.15 kB │ gzip: 0.92 kB
dist/assets/index-Bx7mK3.js 48.72 kB │ gzip: 15.83 kB
✓ built in 1.24s
プロダクションビルドでは以下の最適化が自動的に行われます。
- ミニファイ: JavaScript と CSS の空白・コメントを除去し、変数名を短縮
- Tree Shaking: 使用されていないコードを自動的に除外
- コード分割: 必要に応じてチャンクに分割し、初期ロードを高速化
- ハッシュ付きファイル名: キャッシュバスティングのためにファイル名にハッシュを付与
ビルド結果のプレビュー
デプロイ前にビルド結果をローカルで確認しましょう。
> ビルド結果をローカルでプレビューして
Claude Code が実行するコマンドです。
npm run preview
➜ Local: http://localhost:4173/
http://localhost:4173/ にアクセスして、プロダクションビルドが正しく動作することを確認します。すべての機能(追加・表示・完了・削除・編集・フィルタリング・localStorage 永続化)がローカルプレビューで動作することを確かめてください。
開発サーバーとの違いを意識して確認すべきポイントは以下のとおりです。
- ページの初期読み込みが高速であること
- JavaScript のソースマップがデフォルトでは含まれないこと
- HMR は動作しないこと(本番環境では不要)
- すべての機能が正常に動作すること
Git リポジトリの準備
デプロイの前に、プロジェクトを Git で管理する必要があります。Claude Code に Git の初期化を依頼しましょう。
> Gitリポジトリを初期化して。
.gitignoreは適切に設定して。
初回コミットを作成して。
Claude Code が実行するコマンドです。
git init
Vite が生成した .gitignore には以下が含まれています。
# .gitignore
node_modules
dist
dist-ssr
*.local
node_modules と dist はバージョン管理に含めません。node_modules は npm install で復元でき、dist はビルドで再生成できるためです。
初回コミットを作成します。
git add .
git commit -m "feat: TODOアプリ完成 — Claude Codeで構築"
次に GitHub にリポジトリを作成して push します。
> GitHubに新しいリポジトリを作成してプッシュして。
リポジトリ名は "todo-app-react" で。
gh repo create todo-app-react --public --source=. --push
GitHub CLI(gh)を使えば、コマンドライン上でリポジトリの作成と push が一度に完了します。
Vercel へのデプロイ
Vercel は React アプリを無料でホスティングできるプラットフォームです。Git リポジトリと連携すると、push するたびに自動デプロイが実行されます。
> Vercelにデプロイする手順を教えて。
CLIを使う方法で。
まず Vercel CLI をインストールします。
npm install -g vercel
次にデプロイを実行します。
vercel
初回デプロイ時は対話形式でプロジェクト設定を行います。
? Set up and deploy? [Y/n] Y
? Which scope? your-username
? Link to existing project? N
? What's your project's name? todo-app-react
? In which directory is your code located? ./
? Want to modify these settings? [y/N] N
Vite プロジェクトの場合、Vercel は自動的にビルドコマンド(npm run build)と出力ディレクトリ(dist)を検出してくれます。
デプロイが完了すると、公開 URL が表示されます。
🔗 https://todo-app-react-xxxxx.vercel.app
この URL にアクセスすれば、世界中の誰でもあなたの TODO アプリを使えます。
Netlify へのデプロイ(代替手段)
Vercel の代わりに Netlify を使うこともできます。Claude Code に聞いてみましょう。
> Netlifyにデプロイする方法も教えて。CLIで。
# Netlify CLIのインストール
npm install -g netlify-cli
# デプロイ(手動)
netlify deploy --prod --dir=dist
Netlify もビルド済みの dist ディレクトリを指定するだけでデプロイできます。GitHub 連携による自動デプロイも同様に設定可能です。
もう一つの方法として、GitHub Pages を使うこともできます。
> GitHub Pagesにデプロイする方法を教えて。
vite.config.tsの設定変更も含めて。
GitHub Pages を使う場合は、vite.config.ts に base の設定が必要です。
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
base: '/todo-app-react/', // リポジトリ名
});
デプロイプラットフォームの比較は以下のとおりです。
| プラットフォーム | 無料枠 | 自動デプロイ | カスタムドメイン |
|---|---|---|---|
| Vercel | 無制限(個人) | GitHub 連携 | 無料 |
| Netlify | 月100GBまで | GitHub 連携 | 無料 |
| GitHub Pages | 無制限 | GitHub Actions | 可能 |
コース全体の振り返り
10回の講義を通じて、Claude Code を使いながら TODO アプリを完成させました。学んだ技術とコンセプトを振り返りましょう。
| 講義 | テーマ | 学んだ技術 |
|---|---|---|
| 1 | 環境構築 | Vite, Node.js, Claude Code |
| 2 | コンポーネント設計 | コンポーネントツリー, Props, 型定義 |
| 3 | TODO追加 | useState, フォーム, 制御コンポーネント |
| 4 | 一覧表示 | map(), key, リストレンダリング |
| 5 | 完了・削除 | イミュータブル更新, filter(), イベント処理 |
| 6 | localStorage | useEffect, JSON, カスタムフック |
| 7 | フィルタリング | useMemo, FilterType, 条件分岐 |
| 8 | CSSスタイリング | レスポンシブ, アニメーション, カスタムUI |
| 9 | 編集機能 | useRef, インライン編集, キーボード操作 |
| 10 | ビルド・デプロイ | プロダクションビルド, Vercel, Git |
Claude Code は、自然言語で指示を出すだけでコードを生成・修正してくれる強力なツールです。しかし、生成されたコードの意味を理解することが重要です。「なぜこうなるのか」を常に意識し、分からないことがあれば Claude Code に説明を求めましょう。
演習問題
- カスタムドメイン: 独自ドメインを持っている場合は、Vercel のダッシュボードでカスタムドメインを設定してみましょう。
- 環境変数: Claude Code に「環境変数を使って、アプリのタイトルを設定ファイルから読み込む方法を教えて。Viteの環境変数の仕組みで」と依頼してみましょう。
- CI/CD: Claude Code に「GitHub Actions を使って、push時に自動テストとデプロイを実行するワークフローを作って」と依頼してみましょう。
- 機能拡張: このコースで作った TODO アプリをベースに、以下のいずれかの機能を Claude Code と一緒に追加してみましょう。
- カテゴリ(タグ)機能
- 期限設定機能
- Supabase を使ったクラウド保存
- 複数ユーザー対応(認証機能)