Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

README.md

第5章: React基礎

学習目標

Reactの基礎概念を理解し、本格的なToDoアプリを作成します。

  • なぜReactが必要なのかを理解する
  • 仮想DOMの仕組みを理解する
  • コンポーネントと状態管理の本質を理解する
  • コンポーネント設計
  • 状態管理(useState)
  • 副作用(useEffect)
  • カスタムフック
  • コンポーネントの分割

成果物: 高機能ToDoアプリ


0. なぜReactが必要なのか

0-1. 素のJavaScriptの問題点

シナリオ: 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);
  });
}

問題点:

  1. 全体を毎回再描画: innerHTML = '' で全削除して再作成
  2. コードが散らかる: データ管理とDOM操作が混在
  3. パフォーマンス: 1000個のTodoがあったら、1個変更するだけで1000個再描画
  4. 状態管理が複雑: データがどこにあるか分かりにくい

0-2. Reactの解決策

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>
  );
}

解決されたこと:

  1. 宣言的: 「どう描画するか」ではなく「何を描画するか」を書く
  2. 自動更新: setTodos を呼ぶだけで自動で画面が更新される
  3. 効率的: 変更された部分だけを更新(仮想DOM)
  4. わかりやすい: データと表示が一箇所にまとまっている

0-3. 仮想DOM - Reactの核心技術

仮想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操作
= 速い

0-4. コンポーネント - 再利用可能な部品

コンポーネントとは:

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コンポーネントだけ修正すれば、使っている箇所すべてに反映される

0-5. 状態(State)- データの管理

状態とは:

コンポーネントが持つ「変化するデータ」です。

なぜ状態が必要なのか:

// ❌ これは動かない
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が検知 → 再描画される

1. Reactの基本概念

1-0. Reactの歴史

  • 2013年: Facebook(現Meta)が開発・公開
  • 目的: 複雑なUIを効率的に管理
  • 現在: 世界で最も人気のあるフロントエンドライブラリ

1-1. コンポーネントとは

UIを部品(コンポーネント)として分割して管理します。

// 関数コンポーネント
function Greeting({ name }: { name: string }) {
  return <h1>こんにちは、{name}さん</h1>;
}

// 使用例
<Greeting name="太郎" />

1-2. Props(プロパティ)

親から子へデータを渡します。

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>
  );
}

2. 状態管理(useState)

2-1. 基本的な使い方

'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>
  );
}

2-2. オブジェクトと配列の状態

// オブジェクト
const [user, setUser] = useState({ name: '', email: '' });
setUser({ ...user, name: '太郎' });

// 配列
const [items, setItems] = useState<string[]>([]);
setItems([...items, '新しいアイテム']);
setItems(items.filter((item, index) => index !== 0));

3. 副作用(useEffect)

3-1. 基本的な使い方

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>;
}

3-2. 依存配列

useEffect(() => {
  console.log('countが変更されました');
}, [count]); // countが変わるたびに実行

4. カスタムフック

ロジックを再利用可能な形にします。

// 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', []);
  // ...
}

5. 高機能ToDoアプリの実装

プロジェクト構造

src/
├── app/
│   └── page.tsx
├── components/
│   ├── TodoList.tsx
│   ├── TodoItem.tsx
│   ├── TodoForm.tsx
│   └── TodoFilter.tsx
├── hooks/
│   └── useTodos.ts
└── types/
    └── todo.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';

hooks/useTodos.ts

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
  };
}

ミニ課題

課題1: 編集機能を追加

ToDoをダブルクリックで編集できるようにしてください。

課題2: ソート機能を追加

優先度や作成日でソートできるようにしてください。

解答例
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();
});

よくあるエラーと解決策

エラー1: Too many re-renders

原因: 無限ループ

解決策:

// ❌ 間違い
<button onClick={handleClick()}>

// ✅ 正しい
<button onClick={handleClick}>
<button onClick={() => handleClick()}>

エラー2: Cannot update during render

原因: レンダリング中に状態を更新

解決策: useEffectを使う


まとめ

この章では以下のことを学びました:

  • ✅ Reactコンポーネントの基礎
  • ✅ useState での状態管理
  • ✅ useEffect での副作用処理
  • ✅ カスタムフックの作成
  • ✅ コンポーネント設計

次の章では、Next.jsの機能を学びます。

この章で習得すべきスキル

学習を完了したら、以下の項目をチェックしてください:

基礎知識の理解

  • 宣言的UIと命令的UIの違いを理解している
  • Virtual DOMの仕組みと利点を理解している
  • コンポーネントベースアーキテクチャの利点を理解している
  • Reactの再レンダリングの仕組みを理解している

JSX

  • JSXの基本的な書き方を理解している
  • {} で JavaScript式を埋め込める
  • 条件分岐(&&, 三項演算子)を使える
  • map()でリストをレンダリングできる
  • key propsの重要性を理解している

コンポーネント

  • 関数コンポーネントを作成できる
  • Propsを受け取って使用できる
  • Props の型を TypeScript で定義できる
  • コンポーネントを分割して再利用できる

State管理

  • useStateフックを使える
  • 状態の更新方法を理解している
  • 状態が変更されると再レンダリングされることを理解している
  • イベントハンドラで状態を更新できる

イベント処理

  • onClickイベントを処理できる
  • onChangeイベントを処理できる
  • onSubmitイベントを処理できる
  • event.preventDefault()の使い方を理解している

実践スキル

  • カウンターコンポーネントを作成できる
  • TodoアプリのUIを実装できる
  • フォームの入力を管理できる
  • React Developer Toolsを使ってデバッグできる

次のステップへの準備

  • ルーティングが必要な理由を理解している
  • Next.jsの利点を理解している

参考リンク