Reactの基礎概念を理解し、本格的なToDoアプリを作成します。
- なぜReactが必要なのかを理解する
- 仮想DOMの仕組みを理解する
- コンポーネントと状態管理の本質を理解する
- コンポーネント設計
- 状態管理(useState)
- 副作用(useEffect)
- カスタムフック
- コンポーネントの分割
成果物: 高機能ToDoアプリ
シナリオ: ToDoリストを作る場合
素のJavaScriptの場合:
let todos = [];
function addTodo(text) {
todos.push({ id: Date.now(), text, completed: false });
renderTodos(); // 画面を更新
}
function toggleTodo(id) {
const todo = todos.find(t => t.id === id);
todo.completed = !todo.completed;
renderTodos(); // 画面を更新
}
function renderTodos() {
const container = document.getElementById('todo-list');
container.innerHTML = ''; // 全部消す
todos.forEach(todo => {
const li = document.createElement('li');
li.textContent = todo.text;
li.onclick = () => toggleTodo(todo.id);
container.appendChild(li);
});
}問題点:
- 全体を毎回再描画:
innerHTML = ''で全削除して再作成 - コードが散らかる: データ管理とDOM操作が混在
- パフォーマンス: 1000個のTodoがあったら、1個変更するだけで1000個再描画
- 状態管理が複雑: データがどこにあるか分かりにくい
Reactの場合:
function TodoList() {
const [todos, setTodos] = useState([]);
const addTodo = (text) => {
setTodos([...todos, { id: Date.now(), text, completed: false }]);
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
return (
<ul>
{todos.map(todo => (
<li key={todo.id} onClick={() => toggleTodo(todo.id)}>
{todo.text}
</li>
))}
</ul>
);
}解決されたこと:
- 宣言的: 「どう描画するか」ではなく「何を描画するか」を書く
- 自動更新:
setTodosを呼ぶだけで自動で画面が更新される - 効率的: 変更された部分だけを更新(仮想DOM)
- わかりやすい: データと表示が一箇所にまとまっている
仮想DOMとは:
「実際のDOM」の軽量なコピーです。JavaScriptのオブジェクトで表現されています。
なぜ仮想DOMが速いのか:
従来の方法:
データ変更 → 直接DOMを操作 → ブラウザが再描画(遅い)
Reactの方法:
データ変更 → 仮想DOMを更新(速い)→ 差分だけを実際のDOMに反映 → ブラウザが再描画
視覚的な理解:
1. 初期状態
仮想DOM: 実際のDOM:
<ul> <ul>
<li>タスク1</li> → <li>タスク1</li>
<li>タスク2</li> → <li>タスク2</li>
</ul> </ul>
2. タスク3を追加
仮想DOM(新): 仮想DOM(旧):
<ul> <ul>
<li>タスク1</li> <li>タスク1</li>
<li>タスク2</li> <li>タスク2</li>
<li>タスク3</li> ← 新
</ul> </ul>
3. 差分を計算(Diffing)
React: 「タスク3が追加されただけだ」と認識
4. 最小限の変更だけを実際のDOMに適用
実際のDOM:
<ul>
<li>タスク1</li> ← そのまま
<li>タスク2</li> ← そのまま
<li>タスク3</li> ← 追加のみ
</ul>
なぜこれが速いのか:
- DOMの操作はコストが高い: ブラウザの再描画は遅い
- JavaScriptのオブジェクト操作は速い: 仮想DOMの更新は軽量
- 差分だけ更新: 必要最小限の変更でパフォーマンス最適化
具体的な性能差:
100個のTodoリストで1個を変更する場合:
素のJavaScript:
innerHTML = '' → 100個削除
→ 100個再作成
→ 100個のDOM操作
= 遅い
React:
仮想DOMで差分計算
→ 1個だけ変更を検出
→ 1個のDOM操作
= 速い
コンポーネントとは:
UIを部品化したものです。関数として定義します。
なぜコンポーネントが重要なのか:
1. 再利用できる:
// ボタンコンポーネントを1回定義
function Button({ text, onClick }) {
return (
<button
onClick={onClick}
className="px-4 py-2 bg-blue-500 text-white rounded"
>
{text}
</button>
);
}
// 何度でも使える
<Button text="保存" onClick={handleSave} />
<Button text="キャンセル" onClick={handleCancel} />
<Button text="削除" onClick={handleDelete} />2. テストしやすい:
// コンポーネント単位でテストできる
test('Button renders text correctly', () => {
render(<Button text="テスト" onClick={() => {}} />);
expect(screen.getByText('テスト')).toBeInTheDocument();
});3. 保守しやすい:
// ボタンのデザインを変更したい
// → Buttonコンポーネントだけ修正すれば、使っている箇所すべてに反映される状態とは:
コンポーネントが持つ「変化するデータ」です。
なぜ状態が必要なのか:
// ❌ これは動かない
function Counter() {
let count = 0; // 普通の変数
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => count++}>+1</button>
{/* ボタンを押しても画面は更新されない! */}
</div>
);
}
// ✅ これは動く
function Counter() {
const [count, setCount] = useState(0); // 状態
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
{/* setCountを呼ぶと自動で再描画される */}
</div>
);
}状態が変わると何が起こるか:
1. setCount(新しい値) が呼ばれる
↓
2. Reactが「状態が変わった!」と検知
↓
3. コンポーネント関数を再実行(再レンダリング)
↓
4. 新しい仮想DOMを作成
↓
5. 古い仮想DOMと比較(Diffing)
↓
6. 差分だけを実際のDOMに反映
↓
7. 画面が更新される
なぜsetStateを使うのか:
// 普通の変数の変更はReactが検知できない
let count = 0;
count++; // Reactは気づかない → 再描画されない
// setStateを使うとReactが検知できる
const [count, setCount] = useState(0);
setCount(count + 1); // Reactが検知 → 再描画される- 2013年: Facebook(現Meta)が開発・公開
- 目的: 複雑なUIを効率的に管理
- 現在: 世界で最も人気のあるフロントエンドライブラリ
UIを部品(コンポーネント)として分割して管理します。
// 関数コンポーネント
function Greeting({ name }: { name: string }) {
return <h1>こんにちは、{name}さん</h1>;
}
// 使用例
<Greeting name="太郎" />親から子へデータを渡します。
interface CardProps {
title: string;
description: string;
imageUrl?: string;
}
function Card({ title, description, imageUrl }: CardProps) {
return (
<div className="border rounded-lg p-4">
{imageUrl && <img src={imageUrl} alt={title} />}
<h2>{title}</h2>
<p>{description}</p>
</div>
);
}'use client'
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}// オブジェクト
const [user, setUser] = useState({ name: '', email: '' });
setUser({ ...user, name: '太郎' });
// 配列
const [items, setItems] = useState<string[]>([]);
setItems([...items, '新しいアイテム']);
setItems(items.filter((item, index) => index !== 0));import { useEffect, useState } from 'react';
export default function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// クリーンアップ関数
return () => clearInterval(interval);
}, []); // 空配列 = マウント時のみ実行
return <div>{seconds}秒経過</div>;
}useEffect(() => {
console.log('countが変更されました');
}, [count]); // countが変わるたびに実行ロジックを再利用可能な形にします。
// hooks/useLocalStorage.ts
import { useState, useEffect } from 'react';
export function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
if (typeof window === 'undefined') return initialValue;
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}
// 使用例
function TodoApp() {
const [todos, setTodos] = useLocalStorage<Todo[]>('todos', []);
// ...
}src/
├── app/
│ └── page.tsx
├── components/
│ ├── TodoList.tsx
│ ├── TodoItem.tsx
│ ├── TodoForm.tsx
│ └── TodoFilter.tsx
├── hooks/
│ └── useTodos.ts
└── types/
└── todo.ts
export interface Todo {
id: number;
title: string;
completed: boolean;
createdAt: Date;
priority: 'low' | 'medium' | 'high';
}
export type FilterType = 'all' | 'active' | 'completed';import { useState } from 'react';
import { Todo } from '@/types/todo';
export function useTodos() {
const [todos, setTodos] = useState<Todo[]>([]);
const addTodo = (title: string, priority: Todo['priority'] = 'medium') => {
const newTodo: Todo = {
id: Date.now(),
title,
completed: false,
createdAt: new Date(),
priority
};
setTodos([...todos, newTodo]);
};
const toggleTodo = (id: number) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id: number) => {
setTodos(todos.filter(todo => todo.id !== id));
};
const updateTodo = (id: number, updates: Partial<Todo>) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, ...updates } : todo
));
};
return {
todos,
addTodo,
toggleTodo,
deleteTodo,
updateTodo
};
}ToDoをダブルクリックで編集できるようにしてください。
優先度や作成日でソートできるようにしてください。
解答例
const sortedTodos = [...todos].sort((a, b) => {
if (sortBy === 'priority') {
const priorityOrder = { high: 0, medium: 1, low: 2 };
return priorityOrder[a.priority] - priorityOrder[b.priority];
}
return b.createdAt.getTime() - a.createdAt.getTime();
});原因: 無限ループ
解決策:
// ❌ 間違い
<button onClick={handleClick()}>
// ✅ 正しい
<button onClick={handleClick}>
<button onClick={() => handleClick()}>原因: レンダリング中に状態を更新
解決策: useEffectを使う
この章では以下のことを学びました:
- ✅ Reactコンポーネントの基礎
- ✅ useState での状態管理
- ✅ useEffect での副作用処理
- ✅ カスタムフックの作成
- ✅ コンポーネント設計
次の章では、Next.jsの機能を学びます。
学習を完了したら、以下の項目をチェックしてください:
- 宣言的UIと命令的UIの違いを理解している
- Virtual DOMの仕組みと利点を理解している
- コンポーネントベースアーキテクチャの利点を理解している
- Reactの再レンダリングの仕組みを理解している
- JSXの基本的な書き方を理解している
- {} で JavaScript式を埋め込める
- 条件分岐(&&, 三項演算子)を使える
- map()でリストをレンダリングできる
- key propsの重要性を理解している
- 関数コンポーネントを作成できる
- Propsを受け取って使用できる
- Props の型を TypeScript で定義できる
- コンポーネントを分割して再利用できる
- useStateフックを使える
- 状態の更新方法を理解している
- 状態が変更されると再レンダリングされることを理解している
- イベントハンドラで状態を更新できる
- onClickイベントを処理できる
- onChangeイベントを処理できる
- onSubmitイベントを処理できる
- event.preventDefault()の使い方を理解している
- カウンターコンポーネントを作成できる
- TodoアプリのUIを実装できる
- フォームの入力を管理できる
- React Developer Toolsを使ってデバッグできる
- ルーティングが必要な理由を理解している
- Next.jsの利点を理解している