Skip to content

React チートシート

コンポーネントでユーザーインターフェースを構築するための JavaScript ライブラリ。

01

JSX とコンポーネント

関数コンポーネント

React コンポーネントは JSX を返す JavaScript 関数です。コンポーネント名は大文字で始める必要があります(小文字 = HTML タグ)。Props は属性として渡されパラメータで分割代入されます。JSX は React.createElement() の糖衣構文です。常に単一のルート要素を返します(または Fragment を使用)。コンポーネントは純粋でなければなりません — 同じ props = 同じ出力。

react
// Basic function component
function Welcome({ name }) {
  return <h1>Hello, {name}!</h1>;
}

// Arrow function component
const Greeting = ({ name = 'Guest' }) => (
  <p>Welcome, {name}!</p>
);

// Composing components
function App() {
  return (
    <div>
      <Welcome name="Alice" />
      <Welcome name="Bob" />
      <Greeting />
    </div>
  );
}

JSX 式

JSX は中括弧 {} で JavaScript 式を埋め込むことを許可します。変数、関数呼び出し、三項演算子、値を返す任意の式を配置できます。文(if、for、switch)は直接許可されません — 三項または IIFE を使用してください。ブール値(true、false)、null、undefined は何もレンダリングしません。数値と文字列はテキストとしてレンダリングされます。オブジェクトは有効な React 子ではありません。

react
function Expression({ user, items }) {
  const fullName = user.first + ' ' + user.last;
  const itemCount = items.length;

  return (
    <div>
      {/* Expressions in curly braces */}
      <h1>{fullName}</h1>
      <p>{itemCount} items</p>

      {/* Conditional */}
      <p>{itemCount > 0 ? 'In stock' : 'Sold out'}</p>

      {/* Method calls */}
      <p>{fullName.toUpperCase()}</p>

      {/* Numbers and booleans render as nothing */}
      <p>{false}{null}{undefined}</p>
    </div>
  );
}

Fragment と JSX 内のリスト

Fragment(<>...</>)は余分な DOM ノードを追加せずに複数要素をグループ化します — <div> で囲むよりクリーンです。key を渡す必要がある場合は <Fragment key={...}> を使用します。JSX 要素の配列には一意の key props が必要です。静的リストでは配列インデックスを key として機能しますが、動的リストではレンダリングバグを防ぐため安定した ID を使用してください。Fragment は不要なラッパー要素を減らすことでパフォーマンスを向上させます。

react
// Fragment: group without extra DOM node
function App() {
  return (
    <>
      <header>Header</header>
      <main>Content</main>
      <footer>Footer</footer>
    </>
  );
}

// Array of elements (needs keys)
function List() {
  const fruits = ['Apple', 'Banana', 'Cherry'];
  return (
    <ul>
      {fruits.map((fruit, i) => (
        <li key={i}>{fruit}</li>
      ))}
    </ul>
  );
}

条件付きレンダリング

React は複数の条件付きレンダリングパターンを提供します。if/else ロジックには早期リターン。どちらかには三項(cond ? A : B)。表示/非表示には論理 AND(cond && <Component/>)。複雑な分岐には IIFE。JSX に複雑なロジックを埋め込むのを避け — 変数やヘルパー関数に抽出してください。switch 文にはルックアップオブジェクトや別の関数への抽出を使用します。偽値(0、'')はレンダリングされるため、数値には && ではなく三項を使用してください。

react
function Greeting({ isLoggedIn, user }) {
  // 1. If/else (use early return)
  if (!isLoggedIn) return <Login />;

  // 2. Ternary operator
  return (
    <div>
      {user ? <Dashboard user={user} /> : <Loading />}

      {/* 3. Logical AND (render if truthy) */}
      {user.isAdmin && <AdminPanel />}

      {/* 4. IIFE for complex logic */}
      {(() => {
        if (user.role === 'admin') return <Admin />;
        if (user.role === 'mod') return <Mod />;
        return <User />;
      })()}
    </div>
  );
}

Children と Render Props

children prop は開始タグと終了タグ間の要素を含みます — コンポーザブルコンポーネント(カード、モーダル、レイアウト)に不可欠。Render props はデータを受け取り JSX を返す関数を prop として渡します — ロジック共有の HOC やフックの代替。Render props はフックでは一般的ではありませんが、コンポーネント注入パターンにはまだ有用です。children は明示的に渡す必要のない特別な prop です。

react
// children prop: content between tags
function Card({ title, children }) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-body">{children}</div>
    </div>
  );
}

// Usage
<Card title="Profile">
  <p>Name: Alice</p>
  <p>Age: 30</p>
</Card>

// Render prop pattern
function DataProvider({ render }) {
  const data = fetchData();
  return <div>{render(data)}</div>;
}
02

Props

Props の受け渡し

Props は親から子に渡される読み取り専用データです。任意の JavaScript 値にできます:文字列、数値、ブール値、配列、オブジェクト、関数。文字列値は引用符を使用し(name='Alice')、他のすべての値は中括弧を使用します(age={30})。関数を props にすることで子から親への通信(コールバック)が可能です。Props は下方向に流れます — 子は props を変更できません。双方向データバインディングには、共通の親に状態をリフトします。

react
// Parent passes props to child
function App() {
  return (
    <User
      name="Alice"
      age={30}
      isActive={true}
      tags={['admin', 'dev']}
      onClick={() => console.log('clicked')}
    />
  );
}

// Child receives props
function User({ name, age, isActive, tags, onClick }) {
  return (
    <div onClick={onClick}>
      <h1>{name}</h1>
      <p>Age: {age}</p>
      <p>Status: {isActive ? 'Active' : 'Inactive'}</p>
    </div>
  );
}

デフォルトとオプション Props

デフォルト prop 値は分割代入で設定します(param = defaultValue)。prop が渡されない場合、undefined です。ショートサーキット(bio && <p>)または三項でオプション props を条件付きレンダリングします。PropTypes(レガシー)または TypeScript インターフェースで開発時に prop 型をバリデーションできます。defaultProps(クラスコンポーネント)は関数コンポーネントでは非推奨です — 分割代入のデフォルトを使用してください。

react
// Default values via destructuring
function Button({ color = 'blue', size = 'md', children }) {
  return (
    <button className={'btn btn-' + color + ' btn-' + size}>
      {children}
    </button>
  );
}

// Optional props (undefined if not passed)
function Profile({ name, bio }) {
  return (
    <div>
      <h1>{name}</h1>
      {bio && <p>{bio}</p>}
    </div>
  );
}

// Usage
<Button>Click</Button>           {/* color='blue', size='md' */}
<Profile name="Alice" />         {/* bio is undefined */}

Spread と Rest Props

スプレッド演算子(...props)はすべての props を子要素に渡します — ラッパーコンポーネント(HOC、スタイル付きコンポーネント)に便利。Rest 演算子は特定のものを分割代入した後の残りの props を集めます。このパターンはラッパーコンポーネントが未知の props を DOM 要素に転送するデザインシステムで一般的です。注意:スプレッドは明示的な属性を上書きできます — 順序が重要です({...props} className='x' vs className='x' {...props})。

react
// Spread: pass all props to child
function Input(props) {
  return <input {...props} className="input" />;
}

// Usage
<Input type="text" placeholder="Name" value="Alice" />

// Rest: collect remaining props
function Button({ label, ...rest }) {
  return <button {...rest}>{label}</button>;
}

// Selective spreading
function Card({ title, children, ...divProps }) {
  return (
    <div {...divProps}>
      <h2>{title}</h2>
      {children}
    </div>
  );
}

Prop Types と TypeScript

TypeScript インターフェースは props のコンパイル時型チェックを提供します — 新しい React プロジェクトの推奨アプローチ。オプション props には ? を使用します(isActive?: boolean)。PropTypes はランタイムバリデーション(開発時のみ)を提供し、TypeScript なしの JavaScript プロジェクトに有用です。isRequired は prop が提供されることを保証します。TypeScript はランタイム前に型エラーを捕捉し、大規模コードベースに優れています。

react
// TypeScript interface (recommended)
interface UserProps {
  name: string;
  age: number;
  isActive?: boolean;       // optional
  onClick: (id: number) => void;
}

function User({ name, age, isActive = true, onClick }: UserProps) {
  return <div onClick={() => onClick(1)}>{name}, {age}</div>;
}

// PropTypes (runtime checking, legacy)
import PropTypes from 'prop-types';
User.propTypes = {
  name: PropTypes.string.isRequired,
  age: PropTypes.number,
  isActive: PropTypes.bool,
};

Prop Drilling と Context

Prop drilling は props がそれらを使用しない複数のコンポーネントレイヤーを通過する時に発生します。2-3 レベルなら許容されます。より深いツリーには Context API、状態管理ライブラリ(Redux、Zustand)、またはコンポーネント合成を使用します。合成(props や children としてコンポーネントを渡す)は Context よりエレガントに drilling を解決することが多いです。問うべきこと:すべての中間コンポーネントがこのデータを必要とするか?そうでない場合、コンポーネント構造を再検討してください。

react
// Prop drilling: passing through multiple levels
function App() {
  const [user, setUser] = useState(null);
  return <Layout user={user} />;
}

function Layout({ user }) {
  return <Sidebar user={user} />;
}

function Sidebar({ user }) {
  return <UserInfo user={user} />;
}

// Solution: Context API (see State Management section)
// Avoid drilling more than 2-3 levels
03

useState と State

基本 useState

useState は関数コンポーネントに state を追加する基本フックです。配列 [currentValue, setterFunction] を返します。初期値は任意の型にできます。セッターを呼び出すと新しい値で再レンダリングをトリガーします。State 更新は非同期です — setCount 呼び出し直後に値は変わりません。各コンポーネントインスタンスは独自の独立した state を持ちます。セッターは安定しています(レンダー間で同じ参照)。

react
import { useState } from 'react';

function Counter() {
  // [currentValue, setterFunction] = useState(initialValue)
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount(count - 1)}>-1</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

関数型更新

新しい state が前の state に依存する場合、関数型更新を使用します:setCount(prev => prev + 1)。これにより複数の更新がバッチ化されていても最新の state で作業することが保証されます。関数型更新なしでは、連続した高速呼び出しが古い state を使用する可能性があります。React 18 は state 更新を自動的にバッチ化するため(promise や timeout 内でも)、正確性に関数型更新が不可欠です。

react
function Counter() {
  const [count, setCount] = useState(0);

  // BAD: may not work correctly with rapid updates
  const incrementBad = () => setCount(count + 1);

  // GOOD: functional update uses previous state
  const incrementGood = () => setCount(prev => prev + 1);

  // Batch updates
  const addThree = () => {
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
  };

  return <button onClick={addThree}>Count: {count}</button>;
}

オブジェクトと配列の State

state を直接変更しないでください — 常に新しいオブジェクト/配列を作成します。オブジェクトにはスプレッド演算子で既存のプロパティをコピーします:{...prev, [field]: value}。配列には追加はスプレッド([...prev, newItem])、削除は filter、更新は map を使用します。React は変更を検出するために参照を比較します — 変更されたオブジェクトは同じ参照を持つため React は再レンダリングしません。これは初心者向けの React バグの #1 ソースです。

react
function Form() {
  const [form, setForm] = useState({ name: '', email: '', age: 0 });

  // BAD: mutates state directly
  // form.name = 'Alice'; setForm(form);

  // GOOD: spread to create new object
  const updateField = (field, value) => {
    setForm(prev => ({ ...prev, [field]: value }));
  };

  return (
    <input
      value={form.name}
      onChange={e => updateField('name', e.target.value)}
    />
  );
}

// Array state
function TodoList() {
  const [todos, setTodos] = useState([]);
  const addTodo = (text) => setTodos(prev => [...prev, { id: Date.now(), text }]);
  const removeTodo = (id) => setTodos(prev => prev.filter(t => t.id !== id));
}

遅延初期 State

初期 state に高コストな計算が必要な場合、useState に関数を渡します(遅延初期化)。関数は毎回の再レンダリングではなく最初のレンダリングでのみ実行されます。これは localStorage の解析、IndexedDB からのフェッチ、CPU 集約的なセットアップに重要です。関数形式:useState(() => initialValue)。単純な値(数値、文字列)には値を直接渡してください — 遅延初期化は不要です。

react
import { useState } from 'react';

function ExpensiveInit() {
  // BAD: runs on every render (even though result is ignored)
  const [data, setData] = useState(computeExpensiveValue());

  // GOOD: lazy initialization — function runs only once
  const [data2, setData2] = useState(() => computeExpensiveValue());

  // Reading from localStorage
  const [user, setUser] = useState(() => {
    const saved = localStorage.getItem('user');
    return saved ? JSON.parse(saved) : null;
  });

  return <div>{data2}</div>;
}

複数の State 変数

1 つの大きなオブジェクトではなく、独立した値には複数の useState 呼び出しを使用します。これにより更新がシンプルになり(スプレッド不要)、不要な再レンダリングを防ぎます。関連する値は単一の state オブジェクトにグループ化します(例:フォームフィールド)。複数のサブ値を持つ複雑な state ロジックには useReducer を検討してください。経験則:state 更新が独立なら別々の useState を、関連/相互依存なら useReducer または単一オブジェクトを使用します。

react
function LoginForm() {
  // Multiple independent state variables
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [errors, setErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [rememberMe, setRememberMe] = useState(false);

  const handleSubmit = (e) => {
    e.preventDefault();
    setIsSubmitting(true);
    // ... validation and submission
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={e => setEmail(e.target.value)} />
      <input type="password" value={password}
        onChange={e => setPassword(e.target.value)} />
      <button disabled={isSubmitting}>Submit</button>
    </form>
  );
}
04

useEffect と副作用

基本 useEffect

useEffect はレンダリング後に副作用を実行します。effect 関数はコンポーネントがペイントされた後に実行されます。クリーンアップ関数(返される)は次の effect の前とアンマウント時に実行されます — タイマー、サブスクリプション、リスナーのクリアに不可欠。依存配列が effect の再実行タイミングを制御します:[] = マウント時一度、[dep] = dep 変更時、配列なし = 毎レンダー。メモリリークを防ぐため常にクリーンアップしてください。

react
import { useState, useEffect } from 'react';

function Timer() {
  const [seconds, setSeconds] = useState(0);

  // Runs after every render
  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);

    // Cleanup function runs before next effect or unmount
    return () => clearInterval(interval);
  }, []); // empty array = run once on mount

  return <p>Seconds: {seconds}</p>;
}

依存配列

依存配列は useEffect の動作に重要です。空配列 [] = マウント時のみ(componentDidMount のような)。依存あり [a, b] = マウント時と a または b 変更時。配列なし = 毎レンダー(望むことは稀)。依存の欠落は古いクロージャを引き起こします。不要な依存の含めすぎは過剰な再実行を引き起こします。ミスを捕捉するために exhaustive-deps ESLint ルールを使用します。effect で使用されるコンポーネントスコープからのすべての値が deps にあるべきです。

react
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  // Runs once on mount (empty deps)
  useEffect(() => {
    console.log('Component mounted');
  }, []);

  // Runs when userId changes
  useEffect(() => {
    fetch('/api/users/' + userId)
      .then(r => r.json())
      .then(setUser);
  }, [userId]); // re-run when userId changes

  // Runs on every render (no deps) - rarely needed
  useEffect(() => {
    console.log('Every render');
  });

  return <div>{user?.name}</div>;
}

クリーンアップとサブスクリプション

クリーンアップはサブスクリプション、イベントリスナー、タイマー、WebSocket 接続に不可欠です。クリーンアップなしではメモリリークと重複ハンドラーが発生します。クリーンアップ関数は:(1) 次の effect 再実行の前、(2) コンポーネントアンマウント時に実行されます。WebSocket/イベントリスナーは常にクリーンアップで削除してください。effect に依存する state は、前の roomId からの古いデータの表示を避けるためクリーンアップでリセットします。

react
function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const ws = new WebSocket('wss://chat.example.com/' + roomId);

    ws.onmessage = (event) => {
      setMessages(prev => [...prev, JSON.parse(event.data)]);
    };

    // Cleanup: close connection when roomId changes or unmount
    return () => {
      ws.close();
      setMessages([]); // reset for new room
    };
  }, [roomId]);

  // Window event listener
  useEffect(() => {
    const handleResize = () => console.log(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return <div>{messages.length} messages</div>;
}

データフェッチ

useEffect でのデータフェッチには、アンマウント後の state 設定を防ぐためのキャンセルフラグが必要です(React 警告を引き起こす)。'cancelled' フラグはコンポーネントがまだマウントされている場合のみ setUsers/setError/setLoading が実行されることを保証します。本番アプリには、キャッシュ、重複排除、レース条件を自動処理するデータフェッチライブラリ(React Query、SWR)の使用を検討してください。空の依存配列 [] はマウント時に一度フェッチすることを保証します。

react
function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;

    const fetchData = async () => {
      try {
        setLoading(true);
        const res = await fetch('/api/users');
        const data = await res.json();
        if (!cancelled) setUsers(data);
      } catch (err) {
        if (!cancelled) setError(err.message);
      } finally {
        if (!cancelled) setLoading(false);
      }
    };

    fetchData();
    return () => { cancelled = true; };
  }, []);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

useLayoutEffect vs useEffect

useEffect はブラウザのペイント後に非同期で実行されます — DOM 要素を測定している場合、ユーザーに短いフラッシュが見える可能性があります。useLayoutEffect は DOM 変更後だがペイント前に同期的に実行されます — 視覚的ちらつきを防ぎます。レイアウトに影響する DOM 測定(getBoundingClientRect、スクロール位置)には useLayoutEffect を使用します。それ以外には useEffect を使用します(ペイントをブロックしません)。サーバーでは useLayoutEffect は警告します — useIsomorphicLayoutEffect パターンを使用してください。

react
import { useState, useEffect, useLayoutEffect } from 'react';

function Tooltip({ text }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  // useEffect: runs AFTER paint (user may see flash)
  useEffect(() => {
    const rect = document.getElementById('tip').getBoundingClientRect();
    setPosition({ x: rect.x, y: rect.y });
  }, [text]);

  // useLayoutEffect: runs BEFORE paint (no flash)
  useLayoutEffect(() => {
    const rect = document.getElementById('tip').getBoundingClientRect();
    setPosition({ x: rect.x, y: rect.y });
  }, [text]);

  return <div id="tip" style={{ left: position.x, top: position.y }}>{text}</div>;
}
05

useRef、useMemo と useCallback

useRef の基礎

useRef はレンダー間で持続するミュータブルなオブジェクト { current: value } を返します。state とは異なり、ref.current の変更は再レンダリングをトリガーしません。一般的な用途:(1) DOM 要素へのアクセス(ref 属性経由)、(2) レンダリングに影響しないミュータブル値の格納(タイマー、前の値)、(3) コールバックで使用する最新値の格納。ref オブジェクトはレンダー間で同じアイデンティティを持ちます。初期値は useRef(initialValue) に渡されます。

react
import { useRef } from 'react';

function FocusInput() {
  // ref to access DOM element
  const inputRef = useRef(null);

  const focus = () => inputRef.current.focus();
  const clear = () => {
    inputRef.current.value = '';
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focus}>Focus</button>
      <button onClick={clear}>Clear</button>
    </div>
  );
}

ミュータブル値の useRef

useRef は再レンダリングをトリガーせずにレンダー間で持続するミュータブル値を格納します。これは以下に最適です:タイマー ID、WebSocket 参照、前の state の追跡、レンダー回数のカウント。ref.current の変更は再レンダリングを引き起こさないため、変更時に UI は更新されません — UI に影響する値には state を使用してください。レンダーカウントパターン(ref.current++)はデバッグに有用ですが本番ロジックには使用すべきではありません。

react
function Stopwatch() {
  const [seconds, setSeconds] = useState(0);
  const intervalRef = useRef(null);
  const renderCount = useRef(0);

  // Track render count (doesn't trigger re-render)
  renderCount.current++;

  const start = () => {
    if (intervalRef.current) return;
    intervalRef.current = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);
  };

  const stop = () => {
    clearInterval(intervalRef.current);
    intervalRef.current = null;
  };

  return (
    <div>
      <p>{seconds}s (render #{renderCount.current})</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  );
}

useMemo

useMemo は計算値をメモ化(キャッシュ)し、依存関係が変更された時のみ再計算します。高コストな計算(フィルタリング、ソート、複雑な計算)に使用して毎レンダーでの再実行を回避します。依存配列は useEffect のように動作します。過剰使用はパフォーマンスを損なう可能性があります(メモ化自体にオーバーヘッドがある)— 本当に高コストな操作のみメモ化してください。useMemo は子の再レンダリングを防ぐためのオブジェクト参照の保持にも有用です。

react
import { useState, useMemo } from 'react';

function ProductList({ products, filter }) {
  const [search, setSearch] = useState('');

  // Memoize expensive computation
  const filtered = useMemo(() => {
    console.log('Filtering...');
    return products
      .filter(p => p.category === filter)
      .filter(p => p.name.includes(search));
  }, [products, filter, search]); // recompute only when these change

  // Memoize a value
  const totalPrice = useMemo(() =>
    filtered.reduce((sum, p) => sum + p.price, 0),
  [filtered]);

  return (
    <div>
      <input value={search} onChange={e => setSearch(e.target.value)} />
      <p>Total: ${totalPrice}</p>
      {filtered.map(p => <div key={p.id}>{p.name}</div>)}
    </div>
  );
}

useCallback

useCallback は関数をメモ化し、依存関係が変更されない限りレンダー間で同じ参照を返します。これによりメモ化された子コンポーネント(memo() でラップ)の不要な再レンダリングを防ぎます。useCallback なしでは、親の毎レンダーで新しい関数参照が作成され、memo() 子が再レンダリングされます。最適化された子コンポーネントにコールバックを渡す時に useCallback を使用します。useMemo と同様に過剰使用しないでください — メモ化された子に props として渡される関数のみに。

react
import { useState, useCallback, memo } from 'react';

// Memoized child component
const Button = memo(function Button({ onClick, label }) {
  console.log('Button rendered');
  return <button onClick={onClick}>{label}</button>;
});

function App() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  // Without useCallback: new function every render
  // const handleClick = () => setCount(c => c + 1);

  // With useCallback: stable function reference
  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []); // empty deps = stable forever

  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value)} />
      <Button onClick={handleClick} label="Click" />
      <p>Count: {count}</p>
    </div>
  );
}

Ref のフォワーディング

forwardRef は親コンポーネントが子コンポーネントの DOM 要素に ref を渡すことを可能にします。useImperativeHandle は ref が公開するものをカスタマイズします — DOM ノードの代わりに特定のメソッド(focus、clear、getValue)を公開できます。これは命令的 API を持つ再利用可能な入力コンポーネントの作成に有用です。React 19 は ref を簡素化しました(ref は通常の prop になりました)が、ライブラリには forwardRef がまだ必要です。命令的ハンドルの過剰使用を避けてください — 宣言的 props を優先します。

react
import { useRef, forwardRef, useImperativeHandle } from 'react';

// forwardRef: pass ref to child component
const FancyInput = forwardRef(function FancyInput(props, ref) {
  return <input ref={ref} className="fancy" {...props} />;
});

// useImperativeHandle: expose specific methods
const CustomInput = forwardRef(function CustomInput(props, ref) {
  const inputRef = useRef(null);

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    clear: () => { inputRef.current.value = ''; },
    getValue: () => inputRef.current.value,
  }));

  return <input ref={inputRef} />;
});

// Usage
function App() {
  const ref = useRef(null);
  return (
    <>
      <CustomInput ref={ref} />
      <button onClick={() => ref.current.focus()}>Focus</button>
      <button onClick={() => ref.current.clear()}>Clear</button>
    </>
  );
}
06

useReducer と Context

useReducer の基礎

useReducer は複雑な state ロジックの useState の代替です。Reducer は純粋関数です:(state, action) => newState。Action は何が起きたかを記述し、reducer が state をどう更新するかを決定します。このパターンにより state 遷移が予測可能でテスト可能になります。dispatch は安定しています(同じ参照)。常に新しい state オブジェクトを返してください(変更しない)。デフォルトケースは未知の action に対してエラーをスローすべきです。state が複数のサブ値を持つか次の state が複雑なロジックに依存する時に useReducer を使用します。

react
import { useReducer } from 'react';

// Reducer function: (state, action) => newState
function counterReducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: 0 };
    case 'set':
      return { count: action.payload };
    default:
      throw new Error('Unknown action: ' + action.type);
  }
}

function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
      <button onClick={() => dispatch({ type: 'set', payload: 10 })}>Set 10</button>
    </div>
  );
}

複雑な Reducer

複雑な reducer は複数の関連する state を管理します。各 action タイプは特定の state 遷移を処理します。常に前の state をスプレッドして({...state})無関係なフィールドを保持します。ネストされた更新(todo の切り替えなど)には、map で更新されたアイテムを持つ新しい配列を作成します。Reducer は純粋でなければなりません — 副作用なし、API 呼び出しなし。テスト可能性のために reducer を別ファイルに抽出します。非常に複雑な state には Redux Toolkit のようなライブラリを検討してください。

react
const initialState = {
  todos: [],
  filter: 'all',
  loading: false,
};

function todoReducer(state, action) {
  switch (action.type) {
    case 'add':
      return { ...state, todos: [...state.todos, action.todo] };
    case 'toggle':
      return {
        ...state,
        todos: state.todos.map(t =>
          t.id === action.id ? { ...t, done: !t.done } : t
        ),
      };
    case 'delete':
      return { ...state, todos: state.todos.filter(t => t.id !== action.id) };
    case 'set_filter':
      return { ...state, filter: action.filter };
    case 'set_loading':
      return { ...state, loading: action.loading };
    default:
      return state;
  }
}

function TodoApp() {
  const [state, dispatch] = useReducer(todoReducer, initialState);
  // dispatch({ type: 'add', todo: { id: 1, text: 'Learn React', done: false } })
}

Context API

Context API は prop drilling なしでコンポーネントツリー全体で state を共有します。createContext(defaultValue) で作成します。value prop で Provider でコンシューマーをラップします。useContext(Context) で消費します。Context 値の変更はすべてのコンシューマーの再レンダリングをトリガーします。パフォーマンスのために、コンテキストを分割(ThemeContext、UserContext)してコンポーネントが特定のコンテキスト変更時にのみ再レンダリングするようにします。デフォルト値は Provider がコンシューマーをラップしていない時に使用されます。

react
import { createContext, useContext, useState } from 'react';

// 1. Create context with default value
const ThemeContext = createContext('light');
const UserContext = createContext(null);

// 2. Provider component
function App() {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState(null);

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <UserContext.Provider value={{ user, setUser }}>
        <Page />
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

// 3. Consume context
function Page() {
  const { theme } = useContext(ThemeContext);
  const { user } = useContext(UserContext);
  return <div className={'page theme-' + theme}>Hello {user?.name}</div>;
}

Reducer 付き Context

Context と useReducer の組み合わせは軽量なグローバル state 管理システム(ミニ Redux)を作成します。Provider は state と dispatch の両方を公開します。カスタムフック(useStore)はプロバイダー外で使用された場合のエラーハンドリングを提供します。このパターンは中規模アプリに最適です。非常に大規模で頻繁な更新があるアプリには、すべてのコンシューマーが毎回の state 変更で再レンダリングされるのを避けるため、コンテキストの分割や Redux/Zustand の使用を検討してください。

react
import { createContext, useContext, useReducer } from 'react';

// Combine Context + useReducer for global state
const StoreContext = createContext(null);

function StoreProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <StoreContext.Provider value={{ state, dispatch }}>
      {children}
    </StoreContext.Provider>
  );
}

// Custom hook for easy consumption
function useStore() {
  const context = useContext(StoreContext);
  if (!context) throw new Error('useStore must be used within StoreProvider');
  return context;
}

// Usage
function Component() {
  const { state, dispatch } = useStore();
  return <button onClick={() => dispatch({ type: 'action' })}>Click</button>;
}

useContext パフォーマンス

context 値が変更されると、すべてのコンシューマーが再レンダリングされます — 値の小さな一部のみを使用していても。最適化するには:(1) コンポーネントが必要なもののみサブスクライブするようコンテキストを分割。(2) useMemo で context 値をメモ化して値が実際に変更されていない時の再レンダリングを防止。(3) 細粒度サブスクリプションのためにセレクター(use-context-selector ライブラリ)を使用。高頻度更新(マウス位置など)では Context はパフォーマンス問題を引き起こす可能性があります — ref や外部ストアを検討してください。

react
// SPLIT contexts for performance
const ThemeContext = createContext();
const UserContext = createContext();
const CartContext = createContext();

// Each provider manages its own state
function App() {
  return (
    <ThemeProvider>
      <UserProvider>
        <CartProvider>
          <App />
        </CartProvider>
      </UserProvider>
    </ThemeProvider>
  );
}

// Component only re-renders when its context changes
function ThemedButton() {
  const { theme } = useContext(ThemeContext);
  // Won't re-render when user or cart changes
  return <button className={theme}>Button</button>;
}

// Memoize context value to prevent unnecessary re-renders
function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  const value = useMemo(() => ({ user, setUser }), [user]);
  return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}
07

イベントとフォーム

イベントハンドリング

React イベントは camelCase を使用します(onclick ではなく onClick)。イベントオブジェクトは SyntheticEvent(ネイティブイベントのラッパー)です。e.preventDefault() がデフォルト動作を停止します(フォーム送信、リンクナビゲーション)。e.stopPropagation() がイベントバブリングを防ぎます。ハンドラーにパラメータを渡すには、アロー関数を使用します:onClick={() => handleDelete(id)}。複雑なハンドラーをインラインで定義するのを避け — 可読性のために抽出してください。React イベントはプール化されています(pre-17)ので、非同期アクセスが必要な場合は e.persist() を呼び出してください。

react
function App() {
  // Click event
  const handleClick = (e) => {
    e.preventDefault();
    console.log('Button clicked', e.target);
  };

  // With parameters (use arrow function)
  const handleDelete = (id) => {
    console.log('Delete item', id);
  };

  return (
    <div>
      <button onClick={handleClick}>Click</button>
      <button onClick={() => handleDelete(42)}>Delete</button>
      <div onMouseEnter={() => console.log('hover')}
           onMouseLeave={() => console.log('leave')}>
        Hover me
      </div>
    </div>
  );
}

制御入力

制御入力は値が React state によって制御されます。value prop が入力の値を設定し、onChange が state を更新します。これにより React がフォームデータの「唯一の信頼できる情報源」になります。すべてのキーストロークが state 更新と再レンダリングをトリガーします。複雑なフォームでは冗長になる可能性があります — React Hook Form や Formik のようなライブラリを検討してください。制御入力はリアルタイムバリデーションと動的動作を可能にします。React 警告を避けるため常に value と onChange(または readOnly)を使用してください。

react
function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Email:', email, 'Password:', password);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={e => setEmail(e.target.value)}
        placeholder="Email"
      />
      <input
        type="password"
        value={password}
        onChange={e => setPassword(e.target.value)}
        placeholder="Password"
      />
      <button type="submit">Login</button>
    </form>
  );
}

複数フィールドのフォーム

多くのフィールドを持つフォームには、単一の state オブジェクトと汎用 handleChange 関数を使用します。各入力の name 属性が state キーに一致します。ハンドラーは計算プロパティ名([name]: value)で正しいフィールドを更新します。チェックボックスには value ではなく checked を使用します。このパターンはボイラープレートを大幅に削減します。ファイル入力には非制御入力を使用します(完全には制御できません)。バリデーション付きの複雑なフォームには React Hook Form を検討してください。

react
function RegistrationForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
    country: 'us',
    agree: false,
  });

  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: type === 'checkbox' ? checked : value,
    }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="username" value={formData.username} onChange={handleChange} />
      <input name="email" type="email" value={formData.email} onChange={handleChange} />
      <select name="country" value={formData.country} onChange={handleChange}>
        <option value="us">USA</option>
        <option value="uk">UK</option>
      </select>
      <label>
        <input type="checkbox" name="agree" checked={formData.agree} onChange={handleChange} />
        Agree to terms
      </label>
      <button type="submit">Register</button>
    </form>
  );
}

非制御入力

非制御入力は React state なしで ref を使用して直接 DOM 値にアクセスします。defaultValue prop が初期値を設定します(value ではなく)。これはリアルタイムバリデーションや動的動作が不要なフォームにシンプルです。ファイル入力は非制御でなければなりません(値はセキュリティ上読み取り専用)。非制御入力は非 React コードとの統合にも有用です。トレードオフ:リアルタイムで入力をバリデーションや変換できません。ほとんどのケースで制御入力を優先してください。

react
import { useRef } from 'react';

function UncontrolledForm() {
  const emailRef = useRef(null);
  const passwordRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Email:', emailRef.current.value);
    console.log('Password:', passwordRef.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={emailRef} type="email" defaultValue="" />
      <input ref={passwordRef} type="password" defaultValue="" />
      <button type="submit">Submit</button>
    </form>
  );
}

// File input (must be uncontrolled)
function FileUpload() {
  const fileRef = useRef(null);
  return <input ref={fileRef} type="file" />;
}

バリデーションとエラーハンドリング

フォームバリデーションは送信時または毎変更時に行えます。validate 関数は errors オブジェクトを返します — 空は有効を意味します。各フィールドの横に条件付きでエラーを表示します。より良い UX のため、毎キーストロークではなく blur 時(ユーザーがフィールドを離れた後)にバリデーションします。React Hook Form + Zod や Formik + Yup のようなライブラリは堅牢なバリデーションスキーマ、エラー管理、touch/blur 追跡を提供します。サーバーでも常にバリデーションしてください — クライアントバリデーションは UX 用でセキュリティ用ではありません。

react
function ValidatedForm() {
  const [values, setValues] = useState({ email: '', password: '' });
  const [errors, setErrors] = useState({});

  const validate = () => {
    const errs = {};
    if (!values.email) errs.email = 'Email is required';
    else if (!/\S+@\S+\.\S+/.test(values.email)) errs.email = 'Invalid email';
    if (!values.password) errs.password = 'Password is required';
    else if (values.password.length < 8) errs.password = 'Min 8 characters';
    return errs;
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    const errs = validate();
    setErrors(errs);
    if (Object.keys(errs).length === 0) {
      console.log('Form valid', values);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={values.email}
        onChange={e => setValues(v => ({ ...v, email: e.target.value }))} />
      {errors.email && <span className="error">{errors.email}</span>}
      <input type="password" value={values.password}
        onChange={e => setValues(v => ({ ...v, password: e.target.value }))} />
      {errors.password && <span className="error">{errors.password}</span>}
      <button type="submit">Submit</button>
    </form>
  );
}
08

リストと条件付きレンダリング

リストのレンダリング

.map() で配列を JSX 要素に変換します。各要素には一意の key prop が必要です — 配列インデックスではなく安定した ID(todo.id)を使用します。key は効率的な DOM 更新のために React がどのアイテムが変更されたか(追加、削除、並べ替え)を特定するのを助けます。インデックスを key として使用すると、リストアイテムが並べ替えられたり先頭に挿入されたりした時にバグを引き起こします。空のリストにはフォールバックメッセージをレンダリングします。毎レンダーでの再計算を避けるためフィルタ/ソート済みリストには useMemo を検討してください。

react
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <span>{todo.text}</span>
          <button onClick={() => toggle(todo.id)}>
            {todo.done ? 'Undo' : 'Done'}
          </button>
        </li>
      ))}
    </ul>
  );
}

// Filtering and sorting
function FilteredList({ items, filter }) {
  const visible = items
    .filter(item => item.category === filter)
    .sort((a, b) => a.name.localeCompare(b.name));

  return (
    <ul>
      {visible.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
}

Key の説明

key は兄弟間(同じ親)で一意である必要がありますが、異なるリスト間では繰り返せます。key は React のリコンシリエーションアルゴリズムを助けます:key が変更されると React はコンポーネントを破棄して再作成します(state を失う)。インデックス key では、先頭にアイテムを挿入するとすべてのインデックスがシフトし、React がすべてを再レンダリングします。安定した ID key では、React は新しいアイテムのみをレンダリングします。key はグローバルに一意である必要はありません — リスト内で一意であるだけです。ランダム key(Math.random())を使用しないでください — 毎レンダーで変わります。

react
// GOOD: stable, unique keys
{todos.map(todo => (
  <TodoItem key={todo.id} todo={todo} />
))}

// BAD: index as key (causes bugs with reordering)
{todos.map((todo, index) => (
  <TodoItem key={index} todo={todo} />
))}

// When index keys are OK:
// - Static list (never reordered/filtered)
// - List items have no state
// - List is never prepended to

// Key must be unique among siblings
function List() {
  return (
    <div>
      {users.map(u => <User key={u.id} user={u} />)}
      {posts.map(p => <Post key={p.id} post={p} />)}
      {/* IDs can repeat across different lists */}
    </div>
  );
}

条件付きレンダリングパターン

複数の条件付きレンダリングパターンが存在します。ガード句(ローディング、エラー、認証)には早期リターンが最もクリーンです。コンポーネント中間の if/else には要素変数が機能します。JSX 内のどちらかには三項(cond ? A : B)。表示/非表示には論理 AND(cond && <X/>)。switch のような動作にはオブジェクトルックアップ。ネストされた三項を避けてください — 変数やコンポーネントに抽出します。数値には && ではなく三項を使用してください(0 && <X/> は 0 をレンダリングします)。

react
function UserDashboard({ user, loading, error }) {
  // 1. Early returns for loading/error states
  if (loading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;
  if (!user) return <Login />;

  // 2. Element variables
  let greeting;
  if (user.isAdmin) {
    greeting = <h1>Welcome Admin {user.name}</h1>;
  } else {
    greeting = <h1>Welcome {user.name}</h1>;
  }

  return (
    <div>
      {greeting}

      {/* 3. Ternary for either/or */}
      {user.hasNotifications ? <NotificationBadge /> : null}

      {/* 4. && for show/hide */}
      {user.isAdmin && <AdminPanel />}

      {/* 5. Switch via object lookup */}
      {{ free: <FreePlan />, pro: <ProPlan />, enterprise: <EnterprisePlan /> }
        [user.plan]}
    </div>
  );
}

リストフィルタリングと検索

検索/フィルタ可能なリストはフィルタ用の useState とパフォーマンス用の useMemo を組み合わせます。フィルタ関数はクエリとカテゴリの両方をチェックします。空の状態(結果なし)を常に処理します。大きなリスト(1000+ アイテム)には、表示アイテムのみレンダリングする仮想化(react-window、react-virtualized)を検討してください。API 呼び出しの検索入力をデバウンスします。大文字小文字を区別しない検索には toLowerCase() を使用します。複雑なフィルタリングには別の関数やカスタムフックに抽出します。

react
function SearchableList({ items }) {
  const [query, setQuery] = useState('');
  const [category, setCategory] = useState('all');

  // Memoize filtered results
  const filtered = useMemo(() => {
    return items.filter(item => {
      const matchesQuery = item.name.toLowerCase().includes(query.toLowerCase());
      const matchesCategory = category === 'all' || item.category === category;
      return matchesQuery && matchesCategory;
    });
  }, [items, query, category]);

  return (
    <div>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <select value={category} onChange={e => setCategory(e.target.value)}>
        <option value="all">All</option>
        <option value="food">Food</option>
        <option value="tech">Tech</option>
      </select>

      {filtered.length === 0 ? (
        <p>No results found</p>
      ) : (
        <ul>
          {filtered.map(item => <li key={item.id}>{item.name}</li>)}
        </ul>
      )}
    </div>
  );
}

動的コンポーネント

動的コンポーネントレンダリングはルックアップオブジェクトでタイプをコンポーネントにマッピングします。これは CMS 駆動コンテンツ、フォームビルダー、ページビルダーで一般的です。コンポーネントマップにより長い switch/if チェーンを回避します。未知のタイプは常にフォールバックコンポーネントで処理します。スプレッド props({...block.props})で動的コンポーネントにすべてのプロパティを渡します。このパターンは柔柔軟で拡張可能です — 新しいブロックタイプの追加はマップへの追加のみで済みます。JSX がコンポーネントとして扱うよう変数を大文字化(Component)します。

react
// Render different components based on type
const componentMap = {
  text: TextBlock,
  image: ImageBlock,
  video: VideoBlock,
  quote: QuoteBlock,
};

function ContentRenderer({ blocks }) {
  return (
    <div>
      {blocks.map(block => {
        const Component = componentMap[block.type];
        if (!Component) return <UnknownBlock key={block.id} type={block.type} />;

        return <Component key={block.id} {...block.props} />;
      })}
    </div>
  );
}

// Usage
const blocks = [
  { id: 1, type: 'text', props: { content: 'Hello' } },
  { id: 2, type: 'image', props: { src: 'pic.jpg', alt: 'Picture' } },
];
09

パフォーマンス最適化

React.memo

React.memo は props が変更されていない時に再レンダリングを防ぐようコンポーネントをラップします(浅い比較)。同じ props で頻繁にレンダリングされるコンポーネントに使用します。第 2 引数はカスタム比較関数です:再レンダリングをスキップするには true、再レンダリングするには false を返します。memo はコンポーネントのレンダリングが高コストな場合や頻繁にレンダリングされる親の子の場合にのみ役立ちます。すべてのコンポーネントをラップしないでください — メモ化にはオーバーヘッドがあります。最大効果のために useCallback/useMemo と組み合わせます。

react
import { memo } from 'react';

// Memoized component: only re-renders if props change
const ExpensiveCard = memo(function ExpensiveCard({ title, content }) {
  console.log('Card rendered');
  return (
    <div className="card">
      <h2>{title}</h2>
      <p>{content}</p>
    </div>
  );
});

// Custom comparison function
const MyComponent = memo(function MyComponent(props) {
  return <div>{props.value}</div>;
}, (prevProps, nextProps) => {
  // Return true if props are equal (skip re-render)
  return prevProps.value === nextProps.value;
});

コード分割

コード分割はコンポーネントをオンデマンドで読み込むことで初期バンドルサイズを削減します。React.lazy + Suspense が動的インポートを可能にします。fallback prop はコンポーネント読み込み中に表示されます。ルートベースの分割(ページコンポーネントの遅延読み込み)が最もインパクトが大きいです。コンポーネントベースの分割は即座に必要でない重いコンポーネント(チャート、エディタ)に有用です。各遅延インポートが別のチャンクを作成します。デフォルトエクスポートには React.lazy を使用します。名前付きエクスポートにはモジュールでラップします。

react
import { lazy, Suspense } from 'react';

// Lazy load component (code-split)
const AdminPanel = lazy(() => import('./AdminPanel'));
const Dashboard = lazy(() => import('./Dashboard'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Router>
        <Route path="/admin" element={<AdminPanel />} />
        <Route path="/dashboard" element={<Dashboard />} />
      </Router>
    </Suspense>
  );
}

// Route-based splitting (most common)
// Component-based splitting
const HeavyChart = lazy(() => import('./HeavyChart'));

function Page({ showChart }) {
  return (
    <div>
      {showChart && (
        <Suspense fallback={<Spinner />}>
          <HeavyChart />
        </Suspense>
      )}
    </div>
  );
}

長いリストの仮想化

仮想化は長いリストの表示アイテムのみをレンダリングし、パフォーマンスを劇的に向上させます。react-window と react-virtualized が人気のライブラリです。10,000 の DOM ノードをレンダリングする代わりに、約 12(表示中)のみがスクロール可能なコンテナでレンダリングされます。これにより DOM サイズとレンダリング時間が削減されます。100+ アイテムのリストには仮想化を使用します。トレードオフ:より複雑な実装、検索/検索の潜在的な問題(DOM にないアイテム)。可変高リストには VariableSizeList が必要です。

react
import { FixedSizeList } from 'react-window';

// Virtualized list: only renders visible items
function BigList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      {items[index].name}
    </div>
  );

  return (
    <FixedSizeList
      height={600}
      width="100%"
      itemCount={items.length}
      itemSize={50}
    >
      {Row}
    </FixedSizeList>
  );
}

// Without virtualization: rendering 10,000 items is slow
// With virtualization: only ~12 visible items are rendered

デバウンスとスロットル

デバウンスはアクティビティの停止まで実行を遅延します(例:ユーザーが入力を停止)。スロットルは間隔ごとに 1 回の実行に制限します。両方とも過剰な API 呼び出しや計算を防ぎます。useDebounce フックは指定された遅延の間ユーザーが入力を停止した後にのみデバウンスされた値を更新します。これは検索入力、リサイズハンドラー、スクロールイベントに不可欠です。スロットルには lodash.throttle のようなライブラリを使用するかタイムスタンプで実装します。useEffect で常にタイマーをクリーンアップしてください。

react
import { useState, useEffect } from 'react';

// Debounce hook: delays execution until user stops typing
function useDebounce(value, delay) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debounced;
}

function SearchInput() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 300);

  // API call only fires when user stops typing for 300ms
  useEffect(() => {
    if (debouncedQuery) {
      fetch('/api/search?q=' + debouncedQuery);
    }
  }, [debouncedQuery]);

  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

プロファイリングと最適化

Profiler コンポーネントがレンダリング時間を測定します。phase は 'mount' または 'update' です。actualDuration はミリ秒単位のレンダリング時間です。視覚的なフレームチャートには React DevTools Profiler を使用します。最適化前に、実際のボトルネックを見つけるためにプロファイリングしてください — 推測しないで。一般的なパフォーマンス問題:(1) 不要な再レンダリング(memo/useMemo/useCallback で修正)、(2) 高コストな計算(useMemo で修正)、(3) 大きなリスト(仮想化で修正)、(4) 大きなバンドル(コード分割で修正)。時期尚早の最適化は時間を無駄にします — まず測定してください。

react
import { Profiler } from 'react';

function App() {
  const onRender = (id, phase, actualDuration) => {
    console.log(id + ' ' + phase + ': ' + actualDuration + 'ms');
  };

  return (
    <Profiler id="App" onRender={onRender}>
      <ExpensiveComponent />
    </Profiler>
  );
}

/* Optimization checklist:
1. React.memo for expensive components
2. useMemo for expensive calculations
3. useCallback for props passed to memoized children
4. Code splitting (lazy) for routes
5. Virtualization for long lists
6. Debounce rapid events (search, resize)
7. Avoid inline objects/functions as props
8. Use keys correctly in lists
9. Profile with React DevTools Profiler
10. Check unnecessary re-renders with why-did-you-render
*/
10

パターンとエラーバウンダリ

カスタムフック

カスタムフックは再利用可能なステートフルロジックを 'use' 接頭辞付きの関数に抽出します。他のフックを呼び出せます。カスタムフックはコンポーネント間でロジックを共有する主要な方法です(HOC と render props を置き換え)。複数の値にはオブジェクトを、単一の値には値/配列を返します。ローディングとエラー状態を常に処理します。キャンセルパターン(cancelled フラグ)はアンマウント後の state 更新を防ぎます。ESLint ルールが機能するよう 'use' 接頭辞でフックに名前を付けます。

react
import { useState, useEffect } from 'react';

// Reusable data fetching hook
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    fetch(url)
      .then(res => {
        if (!res.ok) throw new Error('HTTP ' + res.status);
        return res.json();
      })
      .then(data => { if (!cancelled) { setData(data); setError(null); }})
      .catch(err => { if (!cancelled) setError(err.message); })
      .finally(() => { if (!cancelled) setLoading(false); });
    return () => { cancelled = true; };
  }, [url]);

  return { data, loading, error };
}

// Usage
function UserProfile({ id }) {
  const { data: user, loading, error } = useFetch('/api/users/' + id);
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  return <h1>{user.name}</h1>;
}

useLocalStorage フック

useLocalStorage は state を localStorage に永続化します。遅延初期化子がマウント時に localStorage から読み取ります。useEffect は値が変更されるたびに localStorage に書き込みます。try/catch はクォータ超過エラーと JSON 解析エラー(破損データ)を処理します。このパターンは任意の永続 state に機能します:テーマ、ユーザー設定、下書きコンテンツ。SSR 互換性のために typeof window !== 'undefined' をチェックします。クロスタブ同期には 'storage' イベントをリッスンします。類似フック:useSessionStorage、useCookie。

react
import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try {
      const saved = localStorage.getItem(key);
      return saved ? JSON.parse(saved) : initialValue;
    } catch {
      return initialValue;
    }
  });

  useEffect(() => {
    try {
      localStorage.setItem(key, JSON.stringify(value));
    } catch (e) {
      console.error('LocalStorage error:', e);
    }
  }, [key, value]);

  return [value, setValue];
}

// Usage
function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  return (
    <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
      Current: {theme}
    </button>
  );
}

エラーバウンダリ

エラーバウンダリは子コンポーネントの render/ライフサイクルメソッドのエラーをキャッチし、アプリ全体のクラッシュを防ぎます。クラスコンポーネントでなければなりません(フックの同等物はまだありません)。getDerivedStateFromError がフォールバック UI を表示するための state を更新します。componentDidCatch がエラーをログに記録します(Sentry、LogRocket などに送信)。エラーバウンダリはキャッチしません:イベントハンドラー、非同期コード、setTimeout、バウンダリ自体のエラー。特定のセクションをラップして障害を分離します。「Try again」ボタンがエラー状態をリセットします。

react
import { Component } from 'react';

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    console.error('Caught error:', error, errorInfo);
    // Send to error reporting service
    // logErrorToService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        this.props.fallback || (
          <div>
            <h1>Something went wrong.</h1>
            <p>{this.state.error?.message}</p>
            <button onClick={() => this.setState({ hasError: false })}>
              Try again
            </button>
          </div>
        )
      );
    }
    return this.props.children;
  }
}

// Usage: wrap components
<ErrorBoundary fallback={<ErrorPage />}>
  <App />
</ErrorBoundary>

高階コンポーネント(HOC)

高階コンポーネント(HOC)はコンポーネントを取り拡張されたものを返す関数です。フック以前のロジック共有の主要なパターンでした。一般的な用途:認証、ローディング状態、テーマ。HOC は「ラッパーヘル」(深くネストされたコンポーネント)と prop 衝突を引き起こす可能性があります。新しいコードにはカスタムフックを優先してください — よりシンプルでコンポーザブルでコンポーネントツリーに追加されません。HOC はクラスコンポーネントや必要とするライブラリとの統合にはまだ有用です。

react
// HOC: function that takes a component and returns a new one
function withLoading(Component) {
  return function WithLoading({ isLoading, ...props }) {
    if (isLoading) return <div>Loading...</div>;
    return <Component {...props} />;
  };
}

// HOC for authentication
function withAuth(Component) {
  return function WithAuth(props) {
    const { user } = useContext(AuthContext);
    if (!user) return <Redirect to="/login" />;
    return <Component {...props} user={user} />;
  };
}

// Usage
const UserList = withLoading(withAuth(BaseUserList));

// Note: Prefer hooks over HOCs for new code
// HOCs are mainly for class components or library compatibility

複合コンポーネント

複合コンポーネントはユーザーがシンプルな部品から複雑なコンポーネントを構成できるようにします。親(Select)がコンテキストを提供し、子コンポーネント(Trigger、Options、Option)がそれを消費します。このパターンは Radix UI、Headless UI、React Aria のようなライブラリで使用されます。利点:柔軟な API(ユーザーは部品を並べ替え/省略可能)、コンテキスト経由の暗黙的 state 共有、クリーンな JSX。コンポーネントは静的プロパティとして添付されます(Select.Trigger)。これは高度なパターンです — 再利用可能な UI ライブラリに使用し、一回限りのコンポーネントには使用しません。

react
// Compound components: components that work together
function Select({ children, value, onChange }) {
  const [isOpen, setIsOpen] = useState(false);
  const context = { value, onChange, isOpen, setIsOpen };

  return (
    <SelectContext.Provider value={context}>
      <div className="select">{children}</div>
    </SelectContext.Provider>
  );
}

Select.Trigger = function Trigger({ children }) {
  const { isOpen, setIsOpen } = useContext(SelectContext);
  return <button onClick={() => setIsOpen(!isOpen)}>{children}</button>;
};

Select.Options = function Options({ children }) {
  const { isOpen } = useContext(SelectContext);
  return isOpen ? <div className="options">{children}</div> : null;
};

Select.Option = function Option({ value, children }) {
  const { onChange, setIsOpen } = useContext(SelectContext);
  return (
    <div onClick={() => { onChange(value); setIsOpen(false); }}>
      {children}
    </div>
  );
}

// Usage: clean, declarative API
<Select value={val} onChange={setVal}>
  <Select.Trigger>Choose...</Select.Trigger>
  <Select.Options>
    <Select.Option value="a">Option A</Select.Option>
    <Select.Option value="b">Option B</Select.Option>
  </Select.Options>
</Select>
11

カスタムフック

useFetch フック

カスタムフックは再利用可能なステートフルロジックを 'use' 接頭辞付きの関数に抽出します。useFetch はローディング/エラー状態付きでデータフェッチをカプセル化します。AbortController はコンポーネントのアンマウント時や URL 変更時に進行中のリクエストをキャンセルします(レース条件とメモリリークを防止)。非同期操作には常に useEffect でクリーンアップを含めてください。カスタムフックは他のフック(useState、useEffect、useContext)を呼び出せます。これらは render props や HOC なしでコンポーネント間でロジックを共有する主要な方法です。React の rules-of-hooks リンターが機能するよう 'use' 接頭辞で名前を付けます。

react
function useFetch(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let abortController = new AbortController();
    setLoading(true);
    fetch(url, { ...options, signal: abortController.signal })
      .then((res) => {
        if (!res.ok) throw new Error(res.statusText);
        return res.json();
      })
      .then((data) => { setData(data); setError(null); })
      .catch((err) => {
        if (err.name !== "AbortError") setError(err.message);
      })
      .finally(() => setLoading(false));
    return () => abortController.abort();
  }, [url]);

  return { data, loading, error };
}

// Usage
function Profile({ userId }) {
  const { data, loading, error } = useFetch(`/api/users/${userId}`);
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  return <div>{data.name}</div>;
}

useLocalStorage フック

useLocalStorage は React state を localStorage と同期します。遅延初期化子は最初のレンダリングでのみ localStorage から読み取ります。useEffect は値が変更されるたびに localStorage に書き込みます。try/catch は localStorage が満杯または無効な場合(プライベートブラウジング)を処理します。このフックにより永続 state が useState のように簡単になります。クロスタブ同期には storage イベントリスナーを追加します。SSR 安全のために window アクセスをガードします。このパターンは sessionStorage でも機能します — API を入れ替えるだけです。

react
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try {
      const stored = window.localStorage.getItem(key);
      return stored ? JSON.parse(stored) : initialValue;
    } catch {
      return initialValue;
    }
  });

  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (e) {
      console.error("LocalStorage write failed:", e);
    }
  }, [key, value]);

  return [value, setValue];
}

// Usage: persists state across page reloads
function Settings() {
  const [theme, setTheme] = useLocalStorage("theme", "light");
  const [fontSize, setFontSize] = useLocalStorage("fontSize", 14);
  return (
    <div>
      <button onClick={() => setTheme("dark")}>Dark</button>
      <button onClick={() => setTheme("light")}>Light</button>
    </div>
  );
}

useDebounce フック

useDebounce はユーザーが指定された遅延の間入力を停止するまで値の更新を遅延します。これは検索入力、自動保存、ユーザー入力でトリガーされる API 呼び出しに不可欠です — 毎キーストロークでの過剰な呼び出しを防ぎます。クリーンアップ関数は遅延が期限切れする前に値が再変更された場合にタイムアウトをクリアします。デバウンスされた値は一時停止後にのみ更新され、下流の effect(API 呼び出しなど)の頻度を下げます。末尾呼び出し付きの即時実行には useThrottle を使用します。効率的な検索入力補完のために useFetch と組み合わせます。

react
function useDebounce(value, delay = 500) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);
  return debounced;
}

// Usage: debounce search input
function Search() {
  const [query, setQuery] = useState("");
  const debouncedQuery = useDebounce(query, 300);

  useEffect(() => {
    if (debouncedQuery) {
      fetch(`/api/search?q=${debouncedQuery}`)
        .then((res) => res.json())
        .then(setResults);
    }
  }, [debouncedQuery]);

  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

usePrevious フック

usePrevious は useEffect がレンダリング後に実行されるという事実を活用します — レンダリング中は ref.current が古い値を保持し、その後新しい値に更新されます。これは現在と前の state を比較する一般的なパターンです。useWindowSize はリサイズリスナーでビューポート寸法を追跡します。メモリリークを防ぐため useEffect の戻り値で常にイベントリスナーをクリーンアップしてください。これらのユーティリティフックはカスタムフックが DOM 関連ロジックをカプセル化し、コンポーネントをクリーンにし、ロジックを再利用可能でテスト可能にする方法を示します。

react
function usePrevious(value) {
  const ref = useRef(null);
  useEffect(() => {
    ref.current = value; // update AFTER render
  }, [value]);
  return ref.current; // returns previous value during render
}

// Usage: compare current vs previous
function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);

  return (
    <div>
      <p>Now: {count}, before: {prevCount}</p>
      {count > prevCount && <p>Increased!</p>}
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}

// useWindowSize hook
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });
  useEffect(() => {
    const handler = () =>
      setSize({ width: innerWidth, height: innerHeight });
    window.addEventListener("resize", handler);
    return () => removeEventListener("resize", handler);
  }, []);
  return size;
}

useToggle と useClipboard

useToggle は useCallback でラップされた toggle 関数でブール state をシンプルにし、安定したアイデンティティを保証します。useClipboard はクリップボード API を 'copied' フィードバック状態でラップし、タイムアウト後に自動リセットします。これらの小さなユーティリティフックはボイラープレートを削減し、アプリ全体で一般的なパターンを標準化します。両方のフックの useCallback はメモ化された子の不要な再レンダリングを防ぎます。小さく焦点を絞ったフック(useToggle、useClipboard、useMediaQuery、useOnClickOutside)のライブラリを構築すると開発が加速し、一貫した動作が保証されます。

react
function useToggle(initial = false) {
  const [on, setOn] = useState(initial);
  const toggle = useCallback(() => setOn((p) => !p), []);
  return [on, toggle, setOn];
}

function useClipboard(timeout = 2000) {
  const [copied, setCopied] = useState(false);
  const copy = useCallback((text) => {
    navigator.clipboard.writeText(text).then(() => {
      setCopied(true);
      setTimeout(() => setCopied(false), timeout);
    });
  }, [timeout]);
  return [copied, copy];
}

// Usage
function CopyButton({ text }) {
  const [copied, copy] = useClipboard();
  return (
    <button onClick={() => copy(text)}>
      {copied ? "Copied!" : "Copy"}
    </button>
  );
}

function Modal({ children }) {
  const [isOpen, toggle] = useToggle(false);
  return (
    <>
      <button onClick={toggle}>Open</button>
      {isOpen && <div className="modal">{children}</div>}
    </>
  );
}
12

Portals

Portal の作成

createPortal は現在のコンポーネントの階層外の DOM ノード(通常は document.body)に子をレンダリングします。これは親の CSS 制約(overflow: hidden、z-index スタッキングコンテキスト、transform による新しいコンテキスト作成)をエスケープする必要のあるモーダル、ツールチップ、ドロップダウンに不可欠です。DOM の別の場所にレンダリングされるにもかかわらず、portal の React イベントバブリングは元のツリーにあるかのように機能します — 先祖の onClick ハンドラーがまだ発火します。これにより両方の長所が得られます:親制約からの視覚的エスケープ、しかし論理的イベントフローは保持されます。

react
import { createPortal } from "react-dom";

function Modal({ children, onClose }) {
  return createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        <button onClick={onClose}>✕</button>
        {children}
      </div>
    </div>,
    document.body  // render target outside the DOM hierarchy
  );
}

// Usage: the modal renders at body level,
// escaping any overflow:hidden or z-index stacking contexts
function App() {
  const [show, setShow] = useState(false);
  return (
    <div style={{ overflow: "hidden", position: "relative" }}>
      <button onClick={() => setShow(true)}>Open Modal</button>
      {show && <Modal onClose={() => setShow(false)}>Hello!</Modal>}
    </div>
  );
}

Portal とフォーカストラップ付きモーダル

本番モーダルには portal 以上のものが必要です:フォーカス管理(モーダル内にフォーカスをトラップ、クローズ時に復元)、Escape キー処理、ボディスクロールロック、クリック外で閉じる。この実装は以前にフォーカスされた要素を保存し、オープン時にモーダルにフォーカスし、クローズ時にフォーカスを復元します — スクリーンリーダーユーザーに不可欠。body overflow hidden が背景スクロールを防ぎます。クリーンアップ関数がすべてを復元します。完全なフォーカストラッピング(モーダル内のタブサイクル)には focus-trap-react のようなライブラリを使用します。クローズ時に DOM から削除するためクローズ時に null を返します。

react
function Modal({ isOpen, onClose, children }) {
  const modalRef = useRef(null);

  useEffect(() => {
    if (!isOpen) return;
    const modal = modalRef.current;
    const previouslyFocused = document.activeElement;
    modal.focus();

    const handleKey = (e) => {
      if (e.key === "Escape") onClose();
    };
    document.addEventListener("keydown", handleKey);

    // Prevent body scroll
    document.body.style.overflow = "hidden";

    return () => {
      document.removeEventListener("keydown", handleKey);
      document.body.style.overflow = "";
      previouslyFocused.focus(); // restore focus
    };
  }, [isOpen, onClose]);

  if (!isOpen) return null;

  return createPortal(
    <div className="overlay" onClick={onClose}>
      <div ref={modalRef} tabIndex={-1} className="modal">
        {children}
      </div>
    </div>,
    document.body
  );
}

Portal 付きツールチップ

ツールチップは親コンテナをオーバーフローしクリッピングを回避する必要があるため portal の恩恵を受けます。ツールチップ位置はトリガーの getBoundingClientRect() から計算され、position: fixed でボディレベルでレンダリングされます。これにより z-index と overflow の問題を回避します。動的ポジショニング(画面端近くで反転)には Floating UI(旧 Popper.js)のようなライブラリを使用します。portal によりツールチップが overflow: hidden の先祖によってクリップされることはありません。固定配置の座標はビューポートに相対的で、計算が簡単です。

react
function Tooltip({ children, text }) {
  const [visible, setVisible] = useState(false);
  const [coords, setCoords] = useState({ x: 0, y: 0 });
  const targetRef = useRef(null);

  const show = () => {
    const rect = targetRef.current.getBoundingClientRect();
    setCoords({ x: rect.left, y: rect.top - 40 });
    setVisible(true);
  };

  return (
    <>
      <span
        ref={targetRef}
        onMouseEnter={show}
        onMouseLeave={() => setVisible(false)}
      >
        {children}
      </span>
      {visible && createPortal(
        <div style={{ position: "fixed", left: coords.x, top: coords.y }}
             className="tooltip">
          {text}
        </div>,
        document.body
      )}
    </>
  );
}

// Usage
<Tooltip text="Click to save">💾</Tooltip>

Portal 付きドロップダウンメニュー

ドロップダウンメニューはツールチップと同じ overflow/z-index の問題に直面します。portal が視覚的問題を解決します。クリック外ハンドラーはクリックターゲットがトリガー ref の外かチェックします。キャプチャフェーズのスクロールリスナー(第 3 引数として true)は任意のスクロールでメニューを閉じ、メニューがトリガーから切り離されるのを防ぎます。本番用には Floating UI を使用し、エッジ検出、反転、シフト、スクロール/リサイズ時の自動位置更新を処理します。portal + 適切なポジショニングロジック = 任意のレイアウトコンテキストで機能する堅牢なドロップダウン。

react
function Dropdown({ trigger, children }) {
  const [open, setOpen] = useState(false);
  const [pos, setPos] = useState({ top: 0, left: 0 });
  const ref = useRef(null);

  const handleOpen = () => {
    const rect = ref.current.getBoundingClientRect();
    setPos({ top: rect.bottom + 4, left: rect.left });
    setOpen(true);
  };

  useEffect(() => {
    if (!open) return;
    const handleClick = (e) => {
      if (!ref.current?.contains(e.target)) setOpen(false);
    };
    const handleScroll = () => setOpen(false); // close on scroll
    document.addEventListener("mousedown", handleClick);
    window.addEventListener("scroll", handleScroll, true);
    return () => {
      document.removeEventListener("mousedown", handleClick);
      window.removeEventListener("scroll", handleScroll, true);
    };
  }, [open]);

  return (
    <>
      <div ref={ref} onClick={handleOpen}>{trigger}</div>
      {open && createPortal(
        <div style={{ position: "fixed", ...pos }} className="dropdown">
          {children}
        </div>,
        document.body
      )}
    </>
  );
}

Portal イベントバブリング

React Portal の主要な機能:イベントバブリングは DOM ツリーではなく React コンポーネントツリーに従います。子が document.body にポータルされても親コンポーネントの onClick が発火します。これはコンテキスト、state、イベント委譲がすべて自然に機能することを意味します。ただし、CSS 継承は portal 境界を越えません — 異なる DOM サブツリーにあるため親のスタイルはポータルされたコンテンツにカスケードしません。ポータルコンテンツをスタイルするには CSS を明示的に適用する必要があります(クラスまたは :root の CSS 変数経由)。この分離はモーダル/ツールチップには通常望ましいです。

react
function PortalExample() {
  // Despite rendering in document.body, events bubble
  // through the React tree, not the DOM tree
  return (
    <div onClick={() => console.log("Parent clicked!")}>
      <p>Click the button — parent handler fires!</p>
      {createPortal(
        <button onClick={() => console.log("Button clicked!")}>
          I'm in a portal
        </button>,
        document.body
      )}
    </div>
  );
}
// Clicking logs: "Button clicked!" then "Parent clicked!"

// This means context, state, and event delegation
// all work as if the portal were inline

// But CSS inheritance does NOT cross the portal boundary:
// document.body styles won't inherit into the portal content
// unless you explicitly apply them
13

Suspense と遅延読み込み

React.lazy と Suspense

React.lazy はコンポーネントを動的インポートし、オンデマンドで読み込まれる別バンドルを作成します(コード分割)。遅延コンポーネントを <Suspense> でラップし、チャンクダウンロード中に表示される fallback(ローディング状態)を指定します。これにより初期バンドルサイズが削減されます — ユーザーは訪問するページのコードのみダウンロードします。各 lazy() 呼び出しが別チャンクを作成します。ルートベースの分割には各ページコンポーネントを遅延読み込みします。fallback は任意の React ノード(スピナー、スケルトン、テキスト)にできます。Suspense は複数の遅延コンポーネントをラップでき — すべて準備できるまで fallback が表示されます。

react
import { lazy, Suspense } from "react";

// Lazy-load component (code-split)
const Dashboard = lazy(() => import("./Dashboard"));
const Settings = lazy(() => import("./Settings"));

function App() {
  return (
    <Suspense fallback={<div>Loading page...</div>}>
      <nav>
        <button onClick={() => setPage("dash")}>Dashboard</button>
        <button onClick={() => setPage("settings")}>Settings</button>
      </nav>
      {page === "dash" && <Dashboard />}
      {page === "settings" && <Settings />}
    </Suspense>
  );
}

ネストされた Suspense

ネストされた Suspense 境界は各チャンクが読み込まれるにつれてコンテンツが段階的に表示される「ピーリング」効果を作成します。外側の Suspense が最初に fallback を表示し、内側のコンポーネントが読み込まれるにつれて独立して表示されます。これにより単一の遅いコンポーネントがページ全体をブロックするのを防ぎます。Suspense 境界を戦略的に配置します:ルートレベルページの周り(粗)、主要セクションの周り(中)、独立ウィジェットの周り(細)。境界が多すぎるとジャンキーなローディングを作成し、少なすぎると長い待機を作成します。鍵はユーザーが認識するコンテンツ単位に境界を一致させることです。

react
<Suspense fallback={<PageSkeleton />}>
  <Header />
  <Suspense fallback={<MainSkeleton />}>
    <MainContent />  {/* loads first */}
    <Suspense fallback={<CommentsSkeleton />}>
      <Comments />  {/* loads independently, doesn't block MainContent */}
    </Suspense>
  </Suspense>
  <Sidebar />
</Suspense>

// Suspense "peeling" effect:
// 1. PageSkeleton shows
// 2. Header + Sidebar load → PageSkeleton peels away
// 3. MainSkeleton shows until MainContent loads
// 4. MainContent shows, CommentsSkeleton shows
// 5. Comments load → everything visible

エラーバウンダリ付き Lazy

遅延読み込みは失敗する可能性があります(ネットワーク問題、チャンク URL を無効化するデプロイ)。エラーバウンダリはこれらのエラーをキャッチしフォールバック UI を表示します。常に Suspense + lazy を ErrorBoundary でラップしてください。componentDidCatch が監視用にエラーをログに記録します。リトライロジックには、ErrorBoundary の state をリセットするかページをリロードできます。一般的なパターンはチャンクを再インポートするリトライボタンです。エラーバウンダリなしでは、失敗したチャンク読み込みがアプリ全体をクラッシュさせます。これは本番に不可欠です — ネットワーク信頼性は決して 100% ではありません。

react
class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  componentDidCatch(error, info) {
    console.error("Chunk load failed:", error, info);
  }
  render() {
    if (this.state.hasError) {
      return (
        <div>
          <p>Failed to load. {this.state.error.message}</p>
          <button onClick={() => window.location.reload()}>
            Retry
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}

// Wrap lazy components: network failures need handling
<ErrorBoundary>
  <Suspense fallback={<Loader />}>
    <LazyComponent />
  </Suspense>
</ErrorBoundary>

Suspense でのデータフェッチ

React 19 の use() フックはデータフェッチ用の Suspense を可能にします。useEffect とは異なり、use() は promise が解決するまでコンポーネントを一時停止します — 最も近い Suspense 境界が fallback を表示します。同じコンポーネント内の複数の use() 呼び出しは並行解決します(並列フェッチ)。これにより手動のローディング状態管理が排除されます。promise は React 外でキャッシュでき、再レンダー時の再フェッチを防げます。注意:use() は render 内またはフック内でのみ呼び出せます。React 18 には React Query や SWR のような Suspense と統合するライブラリを使用します。

react
// React 18+ Suspense for data fetching (experimental)
import { use } from "react"; // React 19+

// Wrap a promise with use()
function UserProfile({ userId }) {
  // 'use' suspends until the promise resolves
  const user = use(fetchUser(userId));
  return <div>{user.name}</div>;
}

function fetchUser(id) {
  return fetch(`/api/users/${id}`).then((r) => r.json());
}

// Parent provides Suspense boundary
function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <UserProfile userId={1} />
    </Suspense>
  );
}

// Concurrent: multiple suspends resolve together
function Dashboard() {
  const user = use(fetchUser(1));
  const posts = use(fetchPosts(user.id));
  // Both fetch in parallel, Suspense shows until all resolve
  return <div>{user.name}: {posts.length} posts</div>;
}

Suspense List(オーケストレーション)

SuspenseList は複数の Suspense 境界の表示順序をオーケストレートします。revealOrder='forwards' はアイテムを順番に表示します(2 が先に読み込まれても 1 が準備できるまでアイテム 2 は表示されない)— コンテンツジャンプを防ぎます。'together' はすべて表示前に待機します。'backwards' は下から上に表示します。tail='collapsed' はまだ開始していないアイテムのローディング状態を非表示にし、'hidden' はすべての fallback を非表示にします。これは順序が重要なフィードやリストに有用です。注意:SuspenseList は実験的で API が変更される可能性があります — 利用可能性は現在の React ドキュメントを確認してください。

react
// SuspenseList controls reveal order of multiple Suspense
import { SuspenseList, Suspense } from "react";

function Article({ id }) {
  const data = use(fetchArticle(id));
  return <article>{data.title}</article>;
}

function Feed() {
  return (
    <SuspenseList revealOrder="forwards" tail="collapsed">
      {/* "forwards": reveals top-to-bottom as they load */}
      {/* "together": waits for all, reveals together */}
      {/* "backwards": reveals bottom-to-top */}

      <Suspense fallback={<Skeleton />}>
        <Article id={1} />
      </Suspense>
      <Suspense fallback={<Skeleton />}>
        <Article id={2} />
      </Suspense>
      <Suspense fallback={<Skeleton />}>
        <Article id={3} />
      </Suspense>
    </SuspenseList>
  );
}
14

React Router

基本ルーティング

React Router v6 はルートに <BrowserRouter>、ルートマッチング定義に <Routes>、element prop(component ではなく)付きの <Route> を使用します。<Link> は History API を使用するナビゲーションリンクを作成します(ページリロードなし)。動的セグメント(:id)は useParams() でアクセスします。path='*' は 404 用のキャッチオールです。ルートは順序ではなく最適マッチでマッチします。URL 検索パラメータ(?q=search)には useSearchParams() を使用します。BrowserRouter はすべてのルートで index.html を提供するサーバー設定が必要です(SPA フォールバック)。

react
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
        <Link to="/users/123">User 123</Link>
      </nav>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/users/:id" element={<UserPage />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
}

function UserPage() {
  const { id } = useParams();
  return <h1>User ID: {id}</h1>;
}

ネストされたルートと Outlet

ネストされたルートはレイアウト階層を作成します。親ルートの element には子ルートがレンダリングされる <Outlet /> を含める必要があります。インデックスルートは親のパスでレンダリングされます。深くネストされたルート(users/:id)はネストされたレイアウトを作成します — Users レイアウトが UserDetail をラップ。これは永続的なサイドバー/ヘッダーを持つダッシュボードに強力です。useOutlet() で子要素にアクセスします。URL /users/123 は Layout → Users → UserDetail をレンダリングし、それぞれがレイアウトを提供します。これはレイアウトの手動条件付きレンダリングを置き換えます。

react
function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="about" element={<About />} />
          <Route path="users" element={<Users />}>
            <Route path=":id" element={<UserDetail />} />
          </Route>
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

function Layout() {
  return (
    <div>
      <nav>Navigation here</nav>
      <Outlet />  {/* Child routes render here */}
    </div>
  );
}

function Users() {
  return (
    <div>
      <h2>Users</h2>
      <Outlet />  {/* Nested :id route renders here */}
    </div>
  );
}

ナビゲーションとリダイレクト

useNavigate はプログラム的ナビゲーション用の関数を返します。navigate('/path', { replace: true }) は履歴を置き換えます(戻るボタンなし)。次のルートにデータを運ぶため state を渡します(例:ログイン後の戻り先)。<Navigate> は宣言的リダイレクトコンポーネントです — 認証ガード用に render で使用します。NavLink はアクティブリンクのスタイリング用に isActive を提供します。useLocation は現在の URL、pathname、search、hash、state を提供します。アクション後のリダイレクト(フォーム送信)には navigate を使用します。render 内の条件付きリダイレクトには <Navigate> を使用します。

react
import { useNavigate, Navigate, NavLink, useLocation } from "react-router-dom";

function Login() {
  const navigate = useNavigate();
  const location = useLocation();

  const handleLogin = async () => {
    await auth.login();
    // Redirect to intended page or home
    const from = location.state?.from || "/";
    navigate(from, { replace: true });
  };
  return <button onClick={handleLogin}>Login</button>;
}

// Declarative redirect
function ProtectedRoute({ user, children }) {
  if (!user) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
  return children;
}

// NavLink: active styling
<NavLink to="/about" className={({ isActive }) =>
  isActive ? "nav-active" : "nav"
}>
  About
</NavLink>

ローダーとデータ読み込み

React Router v6.4+(データルーター)はローダー(ルートレンダリング前に実行)とアクション(フォーム送信を処理)を追加します。useLoaderData() でローダーデータにアクセスします — ルートデータフェッチ用の useEffect が不要。ローダーはネストされたルートで並行実行します。errorElement がローダー/アクションからのエラーをキャッチします。アクションは <Form method='post'> でフォーム送信を処理します — useActionData() が結果を返します。このパターン(Remix にインスパイア)はデータロジックをルートに併置します。データ API には <BrowserRouter> ではなく createBrowserRouter/createHashRouter が必要です。

react
import { createBrowserRouter, RouterProvider } from "react-router-dom";

const router = createBrowserRouter([
  {
    path: "/users/:id",
    element: <UserPage />,
    loader: async ({ params }) => {
      const res = await fetch(`/api/users/${params.id}`);
      if (!res.ok) throw new Response("Not found", { status: 404 });
      return res.json();
    },
    errorElement: <ErrorPage />,
  },
]);

function UserPage() {
  const user = useLoaderData(); // data from loader
  return <h1>{user.name}</h1>;
}

// Action for form submissions
{
  path: "/users/new",
  element: <NewUser />,
  action: async ({ request }) => {
    const formData = await request.formData();
    const res = await fetch("/api/users", {
      method: "POST",
      body: formData,
    });
    return redirect(`/users/${res.id}`);
  },
}

function App() {
  return <RouterProvider router={router} />;
}

ルートガードと保護されたルート

ルートガードは認証状態やロールに基づいてルートを保護します。RequireAuth は未認証ユーザーをログインにリダイレクトし、ログイン後リダイレクト用に意図された宛先を location.state に保存します。RequireRole はロールベースアクセス制御を追加します。ガードをラップして合成します(RequireAuth > RequireRole > component)。レイアウトルートには element={<RequireAuth><Outlet/></RequireAuth>} ですべての子ルートを一度に保護できます。サーバーでも常に認証をチェックしてください — クライアント側ガードは UX 用でセキュリティ用ではありません。このパターンは任意の条件にスケールします:サブスクリプション、機能フラグなど。

react
function RequireAuth({ children }) {
  const { user } = useAuth();
  const location = useLocation();

  if (!user) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
  return children;
}

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/login" element={<Login />} />
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="dashboard" element={
            <RequireAuth><Dashboard /></RequireAuth>
          } />
          <Route path="admin" element={
            <RequireAuth><RequireRole role="admin"><Admin /></RequireRole></RequireAuth>
          } />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

function RequireRole({ role, children }) {
  const { user } = useAuth();
  if (user?.role !== role) return <Navigate to="/forbidden" />;
  return children;
}
15

状態管理(Context と Redux)

Context API パターン

Context は prop drilling なしでグローバル state を提供します。コンテキストを作成し、Provider でコンシューマーをラップし、useContext でアクセスします。カスタム useAuth フックはエラーチェックを追加し、推奨される API サーフェスです。Context は低頻度更新(認証、テーマ、ロケール)に理想的です。高頻度の state 変更では、Context はすべてのコンシューマーが毎回の変更で再レンダリングします — 複雑な state には useReducer を使用するかコンテキストを分割します。常にプロバイダーを管理する state と併置します。Context 値に関数が含まれる場合 useMemo/useCallback でメモ化すべきです。

react
const AuthContext = createContext(null);

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const login = async (credentials) => {
    const user = await api.login(credentials);
    setUser(user);
  };
  const logout = () => setUser(null);
  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error("useAuth must be inside AuthProvider");
  return ctx;
}

// Usage
function Navbar() {
  const { user, logout } = useAuth();
  return user
    ? <button onClick={logout}>Logout {user.name}</button>
    : <Link to="/login">Login</Link>;
}

// Wrap app: <AuthProvider><App /></AuthProvider>

useReducer + Context

useReducer + Context は外部ライブラリなしの複雑なグローバル state の推奨パターンです。Reducer が state ロジックを一元化します(予測可能でテスト可能な遷移)。プロバイダーは不要な再レンダリングを防ぐため値をメモ化します。派生値(total)は useMemo で計算されます。このパターンはカート、フォーム state、マルチステップウィザードなどを処理します。ミドルウェア、タイムトラベルデバッグ、多くの独立したスライスを持つ本当に複雑なアプリには Redux Toolkit や Zustand を検討してください。しかしほとんどのアプリでは useReducer + Context で十分で依存関係ゼロです。

react
const CartContext = createContext();

function cartReducer(state, action) {
  switch (action.type) {
    case "ADD":
      const existing = state.find((i) => i.id === action.item.id);
      if (existing) {
        return state.map((i) =>
          i.id === action.item.id ? { ...i, qty: i.qty + 1 } : i
        );
      }
      return [...state, { ...action.item, qty: 1 }];
    case "REMOVE":
      return state.filter((i) => i.id !== action.id);
    case "CLEAR":
      return [];
    default:
      return state;
  }
}

function CartProvider({ children }) {
  const [items, dispatch] = useReducer(cartReducer, []);
  const value = useMemo(() => ({
    items,
    total: items.reduce((s, i) => s + i.price * i.qty, 0),
    addItem: (item) => dispatch({ type: "ADD", item }),
    removeItem: (id) => dispatch({ type: "REMOVE", id }),
  }), [items]);
  return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
}

Redux Toolkit の基礎

Redux Toolkit(RTK)は Redux を使用するモダンで推奨される方法です。createSlice がアクションクリエイターと reducer を自動生成します。内部で Immer を使用するため、state を直接「変更」し(state.value += 1)、Immer がイミュータブルな更新を生成します。configureStore が合理的なデフォルト(Redux DevTools、thunk ミドルウェア)でストアをセットアップします。useSelector で state を読み取り、useDispatch でアクションをディスパッチします。RTK は Redux のボイラープレートを排除します(switch 文なし、アクションタイプ定数なし)。非同期ロジックには createAsyncThunk を使用します。RTK Query(含まれる)がデータフェッチとキャッシュを処理します。

react
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { useSelector, useDispatch } from "react-redux";

const counterSlice = createSlice({
  name: "counter",
  initialState: { value: 0 },
  reducers: {
    increment: (state) => { state.value += 1; }, // Immer: mutate safely
    decrement: (state) => { state.value -= 1; },
    addBy: (state, action) => { state.value += action.payload; },
  },
});

const store = configureStore({
  reducer: { counter: counterSlice.reducer },
});

export const { increment, decrement, addBy } = counterSlice.actions;

// Usage in component
function Counter() {
  const count = useSelector((state) => state.counter.value);
  const dispatch = useDispatch();
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(addBy(5))}>+5</button>
    </div>
  );
}

Zustand(軽量代替)

Zustand はミニマルな状態管理ライブラリです — プロバイダーなし、ボイラープレートなし。create() でストアを作成し、セレクター関数付きのフックでアクセスします。セレクターが再レンダリングを防ぎます — 変更されたスライスを使用するコンポーネントのみ再レンダリングします。これは Redux の複雑さなしで Context の再レンダリング問題を解決します。オブジェクトセレクター({a, b} を返す)には、不要な再レンダリングを防ぐため浅い比較を使用します。Zustand はミドルウェア(persist、devtools、immer)をサポートします。Redux が過剰だが Context が多すぎる再レンダリングを引き起こす小〜中規模アプリに理想的です。API は小さいが強力です。

react
import { create } from "zustand";

const useStore = create((set, get) => ({
  count: 0,
  user: null,
  increment: () => set((state) => ({ count: state.count + 1 })),
  setUser: (user) => set({ user }),
  reset: () => set({ count: 0, user: null }),
  // Access other state with get()
  doubleCount: () => get().count * 2,
}));

// Usage: select only what you need (prevents re-renders)
function Counter() {
  const count = useStore((state) => state.count);
  const increment = useStore((state) => state.increment);
  return <button onClick={increment}>{count}</button>;
}

// Multiple selections
function Profile() {
  const { user, setUser } = useStore(
    (state) => ({ user: state.user, setUser: state.setUser })
  );
  // Use shallow comparison for object selectors
  // import { shallow } from "zustand/shallow";
  // useStore(selector, shallow);
}

React Query(サーバー状態)

React Query(TanStack Query)はサーバー状態 — API からフェッチされたデータを管理します。キャッシュ、バックグラウンド再フェッチ、古いデータ、楽観的更新、ページネーションを自動的に処理します。queryKey がキャッシュされたデータを識別します(キャッシュキーのように)。staleTime がデータが新鲜と見なされる期間を制御します。ミューテーション後に invalidateQueries で依存クエリを再フェッチします。Redux(クライアント状態)とは異なり、React Query は非同期サーバーデータ用に特化して構築されています。手動のローディング/エラー状態、useEffect フェッチ、キャッシュ管理を排除します。ほとんどのアプリで React Query + ローカル状態(useState/useReducer)が Redux を完全に置き換えます。

react
import { useQuery, useMutation, QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Users />
    </QueryClientProvider>
  );
}

function Users() {
  const { data, isLoading, error, refetch } = useQuery({
    queryKey: ["users"],
    queryFn: () => fetch("/api/users").then((r) => r.json()),
    staleTime: 60000, // fresh for 60s
    refetchOnWindowFocus: true,
  });

  const mutation = useMutation({
    mutationFn: (newUser) =>
      fetch("/api/users", { method: "POST", body: JSON.stringify(newUser) }),
    onSuccess: () => queryClient.invalidateQueries(["users"]),
  });

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;
  return (
    <div>
      {data.map((u) => <div key={u.id}>{u.name}</div>)}
      <button onClick={() => mutation.mutate({ name: "New" })}>Add</button>
    </div>
  );
}
16

テスト(React Testing Library)

基本コンポーネントテスト

React Testing Library(RTL)はコンポーネントをユーザーが操作するようにテストします — 実装詳細ではなく、ロール、ラベル、テキストで。getByRole が推奨クエリです(アクセシビリティもテスト)。userEvent は fireEvent より正確に実際のユーザーインタラクション(タイピング、クリック)をシミュレートします。テストは内部状態のテストを避け、代わりに可視出力と動作を検証します。ロールでクエリできない場合、getByLabelText、getByText、getByDisplayValue を使用します。必要でない限り getByTestId を避けてください。このアプローチによりテストはリファクタリングに強くなります — ユーザーが見て行うことをテストします。

react
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Counter } from "./Counter";

test("counter increments on click", async () => {
  const user = userEvent.setup();
  render(<Counter />);

  // Find by accessible role (not test-id)
  expect(screen.getByRole("heading")).toHaveTextContent("0");

  await user.click(screen.getByRole("button", { name: /increment/i }));

  expect(screen.getByRole("heading")).toHaveTextContent("1");
});

test("displays error for invalid input", async () => {
  const user = userEvent.setup();
  render(<Form />);

  await user.type(screen.getByLabelText(/email/i), "not-an-email");
  await user.click(screen.getByRole("button", { name: /submit/i }));

  expect(screen.getByRole("alert")).toHaveTextContent(/invalid email/i);
});

フックのテスト

renderHook はカスタムフックを独立してテストします。result.current がフックの戻り値を保持します。すべての state 更新は React が同期的に処理することを保証するため act() でラップする必要があります。rerender で変更する props に依存する effect をテストできます。非同期フック(fetch 付き useEffect)には waitFor または findBy クエリ(更新を待つ)を使用します。フックの直接テストはコンポーネント経由のテストより速く焦点が絞られています。ただし、実際の使用を検証するためコンポーネント統合テストでもフックをテストします。renderHook は @testing-library/react v13+ で利用可能です。

react
import { renderHook, act } from "@testing-library/react";
import { useCounter } from "./useCounter";

test("useCounter increments and decrements", () => {
  const { result } = renderHook(() => useCounter(0));

  expect(result.current.count).toBe(0);

  // Wrap state updates in act()
  act(() => result.current.increment());
  expect(result.current.count).toBe(1);

  act(() => result.current.decrement());
  expect(result.current.count).toBe(0);

  act(() => result.current.reset());
  expect(result.current.count).toBe(0);
});

// Testing with initial props that change
test("useEffect runs on dependency change", () => {
  const { result, rerender } = renderHook(
    ({ id }) => useFetchUser(id),
    { initialProps: { id: 1 } }
  );
  rerender({ id: 2 });
  // Effect re-ran with new id
});

非同期テストとモック

MSW(Mock Service Worker)はサービスワーカーレベルでネットワークリクエストをインターセプトします — テストは実際の fetch() を使用しますがモックされたレスポンスを得ます。これは fetch を直接モックするより現実的です。Node(Jest)用に setupServer、ブラウザ用に setupWorker。beforeAll/afterAll ライフサイクルがサーバーを管理します。server.use() でテストごとにハンドラーを上書きします。findBy クエリ(非同期)は要素の出現を待ちます — 非同期レンダリングに使用します。queryBy は見つからない場合 null を返します(不在のアサーション用)。waitFor は条件をポーリングします。MSW は開発モックや Storybook にも使用できます。

react
import { render, screen, waitFor } from "@testing-library/react";
import { rest } from "msw";
import { setupServer } from "msw/node";
import { UserProfile } from "./UserProfile";

// Mock API with MSW (Mock Service Worker)
const server = setupServer(
  rest.get("/api/users/:id", (req, res, ctx) => {
    return res(ctx.json({ id: 1, name: "Alice" }));
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

test("displays user after fetch", async () => {
  render(<UserProfile id={1} />);

  // findBy waits for async update
  expect(await screen.findByText("Alice")).toBeInTheDocument();
  expect(screen.queryByText("Loading")).not.toBeInTheDocument();
});

test("shows error on fetch failure", async () => {
  server.use(
    rest.get("/api/users/:id", (req, res, ctx) =>
      res(ctx.status(500))
    )
  );
  render(<UserProfile id={1} />);
  expect(await screen.findByText(/error/i)).toBeInTheDocument();
});

Context と Provider のテスト

必要なプロバイダー(Theme、Auth、Router など)でコンポーネントをラップするカスタム render ユーティリティを作成します。これにより各テストでプロバイダーセットアップを繰り返すのを避けます。test-utils ファイルから RTL 関数を再エクスポートし、テストがそこからインポートするようにします。ルーターテストには MemoryRouter(BrowserRouter ではなく)と initialEntries で開始 URL を設定します — 実際のブラウザ履歴不要。Redux には実際またはモックストアでテスト Provider でラップします。このパターンはテストをクリーンに保ち、すべてのコンポーネントが必要なコンテキストを持つことを保証します。これは任意の React テストインフラの標準セットアップです。

react
// Custom render that wraps with providers
import { render } from "@testing-library/react";
import { ThemeProvider } from "./ThemeProvider";

function customRender(ui, { theme = "light", ...options } = {}) {
  function Wrapper({ children }) {
    return <ThemeProvider initialTheme={theme}>{children}</ThemeProvider>;
  }
  return render(ui, { wrapper: Wrapper, ...options });
}

// Re-export everything
export * from "@testing-library/react";
export { customRender as render };

// In test files, import from your test-utils:
// import { render, screen } from "../test-utils";

test("button uses theme color", () => {
  customRender(<Button>Click</Button>, { theme: "dark" });
  expect(screen.getByRole("button")).toHaveClass("btn-dark");
});

// Testing with router
import { MemoryRouter } from "react-router-dom";
render(
  <MemoryRouter initialEntries={["/users/123"]}>
    <App />
  </MemoryRouter>
);

イベントとインタラクションのテスト

userEvent.setup() はリアルなインタラクションのユーザーインスタンスを作成します:type(文字ごと)、click、tab、keyboard({Escape}、{Enter} のようなキーコード付き)、selectOptions、upload など。常にユーザーインタラクションを await してください — 非同期です。キーボードナビゲーションのテストはアクセシビリティに不可欠です。フォームテストでは、すべてのフィールドに入力し onSubmit が正しいデータを受け取ることを検証します。userEvent は実際のブラウザ動作(正しい順序の focus、blur、input イベント)をシミュレートするため fireEvent より推奨されます。個別のイベントハンドラーではなく、完全なユーザーフローをテストします。

react
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

test("form submission flow", async () => {
  const onSubmit = jest.fn();
  const user = userEvent.setup();
  render(<LoginForm onSubmit={onSubmit} />);

  // Fill form fields
  await user.type(screen.getByLabelText(/email/i), "[email protected]");
  await user.type(screen.getByLabelText(/password/i), "password123");

  // Submit
  await user.click(screen.getByRole("button", { name: /login/i }));

  expect(onSubmit).toHaveBeenCalledWith({
    email: "[email protected]",
    password: "password123",
  });
});

test("keyboard navigation", async () => {
  const user = userEvent.setup();
  render(<Modal />);
  await user.tab(); // focus first element
  expect(screen.getByRole("button", { name: /close/i })).toHaveFocus();
  await user.keyboard("{Escape}"); // press Escape
  expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
17

TypeScript + React

コンポーネント Props の型付け

React での TypeScript は props の型安全性を提供します。props にインターフェースまたは型エイリアスを使用します。オプション props には ? を使用します。ユニオン型(variant)が値を制約します。React.ReactNode は任意のレンダリング可能なコンテンツ(文字列、要素、配列)を受け入れます。HTML 属性の拡張(React.InputHTMLAttributes)でコンポーネントがすべてのネイティブ属性(placeholder、onChange など)を受け取りつつカスタム props を追加できます。スプレッド {...rest} で残りの属性をネイティブ要素に渡します。このパターンは型安全で柔軟なコンポーネントを作成します。コンシューマーが参照できるよう prop 型を常にエクスポートしてください。

react
// Basic props
interface ButtonProps {
  text: string;
  onClick: () => void;
  variant?: "primary" | "secondary"; // union type
  disabled?: boolean;
}

function Button({ text, onClick, variant = "primary", disabled }: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant}`}
      onClick={onClick}
      disabled={disabled}
    >
      {text}
    </button>
  );
}

// Children prop
interface CardProps {
  title: string;
  children: React.ReactNode; // any renderable content
}

// Extending HTML attributes
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label: string;
  error?: string;
}

function Input({ label, error, ...rest }: InputProps) {
  return (
    <label>
      {label}
      <input {...rest} />
      {error && <span className="error">{error}</span>}
    </label>
  );
}

TypeScript 付きフック

TypeScript はフックに型安全性を追加します。useState<T> で state 型を指定し、nullable state には useState<T | null>(null) を使用します。useRef<T>(null) で ref を型付けし — current は T | null です。useContext にはコンテキスト型を定義し undefined の場合はスローします(コンシューマーが非 undefined 型を得るため)。useReducer には Action を判別共用体型として型付けします — action.type の switch で各ケースの型を絞り込み、型安全なペイロードアクセスを提供します。これらのパターンは undefined アクセスや不正なアクションペイロードからのランタイムエラーを排除します。

react
// useState with types
const [count, setCount] = useState<number>(0);
const [user, setUser] = useState<User | null>(null);
const [items, setItems] = useState<string[]>([]);

// useRef
const inputRef = useRef<HTMLInputElement>(null);
// Access: inputRef.current?.focus()

// useContext
interface ThemeContextType {
  theme: "light" | "dark";
  toggle: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error("useTheme must be inside ThemeProvider");
  return ctx; // ctx is now ThemeContextType, not undefined
}

// useReducer
type Action = { type: "increment" } | { type: "set"; value: number };
const [state, dispatch] = useReducer((state: number, action: Action) => {
  switch (action.type) {
    case "increment": return state + 1;
    case "set": return action.value;
  }
}, 0);

ジェネリックコンポーネント

ジェネリックコンポーネントとフックは型安全性を維持しつつ任意のデータ型で動作します。<T> 型パラメータは items prop から推論されるため、renderItem と keyExtractor が自動的に正しい型を受け取ります。これが TypeScript が完全な型安全性で汎用ユーティリティコンポーネント(List、Table、Select)を再作成する方法です。ジェネリックフック(useArray<T>)も同様に操作を通じて型を保持します。重要な洞察:TypeScript は使用から T を推論するため、コンシューマーは明示的に指定する必要が稀です。このパターンは再利用可能で型安全なコンポーネントライブラリの構築に不可欠です。

react
// Generic component: works with any type
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map((item) => (
        <li key={keyExtractor(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// Usage: TypeScript infers T from items
<List
  items={[{ id: "1", name: "Alice" }, { id: "2", name: "Bob" }]}
  renderItem={(user) => <span>{user.name}</span>}
  keyExtractor={(user) => user.id}
/>

// Generic hook
function useArray<T>(initial: T[]) {
  const [array, setArray] = useState(initial);
  const push = (item: T) => setArray((prev) => [...prev, item]);
  return { array, push, setArray };
}

イベント型と Ref

React イベント型は特定です:入力には ChangeEvent、フォームには FormEvent、クリックには MouseEvent。それぞれが要素型でジェネリックです(e.target が正しく型付けされる)。TypeScript 付き forwardRef には 2 つの型パラメータが必要です:ref 型と props 型。forwardRef はコンポーネントが DOM 要素に ref を公開する必要がある場合(focus、測定など)に必要です。より良い DevTools デバッグのために forwardRef/memo コンポーネントに常に displayName を設定します。React 19 は ref を通常の prop として許可し forwardRef の必要性を減らしますが、既存のコードではまだ一般的です。

react
// Event types
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
  console.log(e.target.value); // string
}

function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
  e.preventDefault();
  const formData = new FormData(e.currentTarget);
}

function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
  console.log(e.clientX, e.clientY);
}

// forwardRef with TypeScript
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label: string;
}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ label, ...props }, ref) => (
    <label>
      {label}
      <input ref={ref} {...props} />
    </label>
  )
);
Input.displayName = "Input";

// Usage: const ref = useRef<HTMLInputElement>(null);
// <Input ref={ref} label="Email" />

Props のユーティリティ型

TypeScript ユーティリティ型は prop 合成に強力です。Pick は特定の props を選択します(サブセットコンポーネント用)。Omit は props を除外します(動作の置き換え用)。Partial はすべての props をオプションにします(デフォルト prop パターン用)。ComponentProps<typeof Component> はコンポーネントの prop 型を抽出します — 既存コンポーネントのラップ/拡張に有用。Record<K, V> はキーを値にマッピングする型を作成します(バリアントからクラスへのマップに最適)。これらのユーティリティによりインターフェースの繰り返しなしで DRY で型安全な prop 定義が可能になります。メンテナブルな React + TypeScript コードを書くためにマスターしてください。

react
// Pick: select specific props
interface ButtonProps {
  text: string;
  onClick: () => void;
  color: string;
  size: "sm" | "md" | "lg";
}
type IconButtonProps = Pick<ButtonProps, "onClick" | "size"> & {
  icon: React.ReactNode;
};

// Omit: exclude specific props
type LinkButtonProps = Omit<ButtonProps, "onClick"> & {
  href: string;
};

// Partial: all props optional (for defaults)
type DefaultProps = Partial<ButtonProps>;

// ComponentProps: extract props from existing component
type MyButtonProps = React.ComponentProps<typeof Button> & {
  variant?: "custom";
};

// ReturnType: type of a function's return
type User = ReturnType<typeof fetchUser>;

// Record for prop maps
type ButtonVariants = Record<"primary" | "danger" | "ghost", string>;
18

Concurrent Features(useTransition、useDeferredValue)

useTransition

useTransition は state 更新を非緊急(トランジション)としてマークします。緊急更新(入力値)は応答性のために即座にレンダリングされ、非緊急更新(10,000 アイテムのフィルタリング)はユーザーが再入力した場合に中断できます。isPending はトランジションが進行中であることを示します(控えめなローディングインジケーターを表示)。これにより高コストなレンダリング中の UI フリーズを防ぎます。重要な洞察:React は古いトランジションを中断して破棄でき、UI の応答性を保ちます。検索フィルタリング、タブ切り替え、重いレンダリングをトリガーする state 更新に使用します。緊急更新(タイピング、クリック)をトランジションでラップしないでください。

react
import { useTransition, useState } from "react";

function SearchResults() {
  const [isPending, startTransition] = useTransition();
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);

  const handleSearch = (value) => {
    setQuery(value); // urgent: update input immediately
    startTransition(() => {
      // non-urgent: heavy filtering can be interrupted
      const filtered = heavyFilter(allItems, value);
      setResults(filtered);
    });
  };

  return (
    <div>
      <input value={query} onChange={(e) => handleSearch(e.target.value)} />
      {isPending && <span>Updating...</span>}
      <ul>{results.map((r) => <li key={r.id}>{r.name}</li>)}</ul>
    </div>
  );
}

useDeferredValue

useDeferredValue は useTransition の宣言的対応物です。低優先度で更新される値の遅延コピーを返します。入力は即座に更新され(緊急)、高コストなリストは遅延値で再レンダリングされます(非緊急)。Results コンポーネントの React.memo が重要です — 毎キーストロークでの再レンダリングを防ぎ、deferredQuery 変更時のみ再レンダリングします。isStale(現在と遅延の比較)で視覚インジケーター(薄暗く、スピナー)を表示できます。state 更新を制御できない場合(例:値が props から来る)に useDeferredValue を使用します。更新を制御する場合に useTransition を使用します。

react
function Search() {
  const [query, setQuery] = useState("");
  // query updates immediately; deferredQuery lags behind
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <Results query={deferredQuery} isStale={isStale} />
    </div>
  );
}

// Memoize the expensive component so it only re-renders
// when deferredQuery changes (not on every keystroke)
const Results = React.memo(function Results({ query, isStale }) {
  const items = expensiveSearch(query); // heavy computation
  return (
    <div style={{ opacity: isStale ? 0.5 : 1 }}>
      {items.map((i) => <div key={i.id}>{i.name}</div>)}
    </div>
  );
});

useOptimistic(React 19)

useOptimistic(React 19)は楽観的更新を実装します — UI は期待される結果で即座に更新され、その後実際のサーバーレスポンスで調整されます。楽観的 state は非同期操作中に表示され、実際のデータが到着した時(コンポーネントが新しい props で再レンダリングされる時)に楽観的値が自動的に置き換えられます。操作が失敗した場合、楽観的更新は変更されていない props での次のレンダリングで単純に元に戻ります。これにより手動の楽観的更新ロジック(保留状態の追跡、エラー時の元に戻し)が排除されます。保留中のアイテムをマーク(pending: true)してローディングインジケーターを表示します。いいね、コメント、トグルに完璧です。

react
import { useOptimistic } from "react";

function ThumbsUp({ likes, addLike }) {
  // Optimistic state: updates immediately, reverts on error
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    likes,
    (state, newLike) => [...state, newLike]
  );

  const handleClick = async () => {
    const newLike = { id: Date.now(), pending: true };
    addOptimisticLike(newLike); // instant UI update
    try {
      await addLike(newLike); // actual API call
    } catch {
      // Reverts automatically on re-render with real data
    }
  };

  return (
    <div>
      <button onClick={handleClick}>👍 {optimisticLikes.length}</button>
      {optimisticLikes.some((l) => l.pending) && <span>Saving...</span>}
    </div>
  );
}

use(React 19 フック)

use() は React 19 の新しいフックで、コンテキストまたは promise を読み取ります。useContext とは異なり、use() は条件的に呼び出せます(if 文、ループ内)— rules-of-hooks 制限がありません。promise の場合、use() は promise が解決するまでコンポーネントを一時停止します(Suspense 境界が必要)。promise は親で作成され prop として渡されます — これによりレンダリング中にフェッチが開始され(useEffect 内ではなく)、ウォーターフォールがより早く開始されます。同じ promise を複数のコンポーネントに渡せます(重複排除)。use() は同期的コンテキストと非同期データのギャップを埋めます。

react
import { use } from "react";

// Read context with use (works in conditions!)
function Theme() {
  // Unlike useContext, use() can be inside conditions
  if (showTheme) {
    const theme = use(ThemeContext); // conditional context!
    return <div style={{ background: theme.color }} />;
  }
  return null;
}

// Read promises with use (Suspense integration)
function UserProfile({ userPromise }) {
  // Suspends until promise resolves
  const user = use(userPromise);
  return <h1>{user.name}</h1>;
}

// Parent passes promise (starts fetching during render)
function App() {
  const userPromise = fetchUser(); // starts immediately
  return (
    <Suspense fallback={<Loading />}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

Concurrent レンダリングパターン

Concurrent パターン:非緊急タブ切り替えに useTransition(重いコンテンツのレンダリング中もナビを応答性に保つ)。useSyncExternalStore は concurrent モードで外部ストア(ブラウザ API、Redux、Zustand)に安全にサブスクライブします — クライアントとサーバー用のスナップショット関数を提供します(SSR セーフ)。レンダリング中に外部のミュータブル state を直接使用しないでください(ティアリングリスク)。常に useSyncExternalStore を通じてください。3 つの引数:subscribe(クリーンアップを返す)、getSnapshot(現在値)、getServerSnapshot(SSR 初期値)。これにより concurrent レンダリング中の一貫した読み取りが保証されます。Redux や Zustand のようなライブラリはこれを内部で使用します。

react
// Pattern 1: Deferred search with transition
function App() {
  const [tab, setTab] = useState("home");
  const [isPending, startTransition] = useTransition();

  return (
    <>
      <nav>
        <button
          onClick={() => startTransition(() => setTab("analytics"))}
          disabled={isPending}
        >
          {isPending ? "Loading..." : "Analytics"}
        </button>
      </nav>
      {tab === "home" && <Home />}
      {tab === "analytics" && <HeavyAnalytics />}
    </>
  );
}

// Pattern 2: useSyncExternalStore for external state
function useOnlineStatus() {
  return useSyncExternalStore(
    (callback) => {
      window.addEventListener("online", callback);
      window.addEventListener("offline", callback);
      return () => {
        window.removeEventListener("online", callback);
        window.removeEventListener("offline", callback);
      };
    },
    () => navigator.onLine, // client snapshot
    () => true // server snapshot (SSR)
  );
}
19

Context API 詳細

createContext と Provider

createContext は Provider が見つからない時に使用されるデフォルト値を持つコンテキストオブジェクトを作成します。Provider の value prop はすべての子孫に消費されます。不要な再レンダリングを防ぐため値を useCallback/useMemo でラップします。

react
const ThemeContext = React.createContext({ theme: 'light', toggle: () => {} });

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const toggle = useCallback(() => setTheme((t) => (t === 'light' ? 'dark' : 'light')), []);
  return (
    <ThemeContext.Provider value={{ theme, toggle }}>
      {children}
    </ThemeContext.Provider>
  );
}

useContext

useContext は最も近い Provider 値を読み取り、その値が変更された時にコンポーネントを再レンダリングします。異なる懸念事項のために複数の Provider をネストします。パフォーマンスのために更新頻度でコンテキストを分割します。

react
function ThemedButton() {
  const { theme, toggle } = useContext(ThemeContext);
  return (
    <button onClick={toggle} style={{ background: theme === 'dark' ? '#333' : '#eee' }}>
      Toggle Theme
    </button>
  );
}

Context と Reducer

useReducer と Context のペアリングは Redux なしでグローバルストアを作成します。Reducer が state ロジックを一元化し、Context が state と dispatch を配信します。コンシューマーは prop drilling なしでアクションをディスパッチできます。

react
const StoreContext = React.createContext(null);

function storeReducer(state, action) {
  switch (action.type) {
    case 'add': return { items: [...state.items, action.item] };
    case 'remove': return { items: state.items.filter((_, i) => i !== action.index) };
    default: return state;
  }
}

function StoreProvider({ children }) {
  const [state, dispatch] = useReducer(storeReducer, { items: [] });
  return <StoreContext.Provider value={{ state, dispatch }}>{children}</StoreContext.Provider>;
}

Context レンダリングの最適化

state と dispatch が同じコンテキストにある場合、すべての state 変更がすべてのコンシューマーを再レンダリングします。分割することで dispatch のみのコンポーネントは state 変更で再レンダリングしません。useReducer の dispatch は安定しています。

react
const StateContext = React.createContext(null);
const DispatchContext = React.createContext(null);

function Provider({ children }) {
  const [state, dispatch] = useReducer(reducer, initial);
  return (
    <StateContext.Provider value={state}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  );
}

Context 用カスタムフック

useContext をカスタムフックでラップするとクリーンな API と Provider 欠落時の明確なエラーを提供します。Provider とフックの両方をエクスポートします。これがコンテキストを消費する推奨方法です。

react
export function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error('useAuth must be used within AuthProvider');
  return ctx;
}
20

useReducer

基本 useReducer

useReducer は複雑な state ロジックの useState の代替です。Reducer は純粋関数です:(state, action) => newState。dispatch は安定しているため、再レンダリングを気にせずに渡せます。

react
function reducer(state, action) {
  switch (action.type) {
    case 'increment': return { count: state.count + 1 };
    case 'decrement': return { count: state.count - 1 };
    case 'reset':     return { count: 0 };
    default: throw new Error('Unknown action: ' + action.type);
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  return <button onClick={() => dispatch({ type: 'increment' })}>{state.count}</button>;
}

遅延初期化

useReducer の第 3 引数は初期レンダリング時に一度実行される init 関数です。これは初期 state の計算が高コストな場合や reset が計算された状態に戻りたい場合に有用です。

react
function init(initialCount) {
  return { count: initialCount, history: [] };
}

function reducer(state, action) {
  switch (action.type) {
    case 'increment': return { ...state, count: state.count + 1 };
    case 'reset':     return init(action.payload);
    default: return state;
  }
}

const [state, dispatch] = useReducer(reducer, initialCount, init);

複雑な State 構造

useReducer は state が複数の関連フィールドを持つ時に輝きます。各アクションは完全な state 遷移を記述し、散在する setState 呼び出しよりロジックを追跡しやすくします。Reducer を純粋に保ってください。

react
const initialState = { users: [], loading: false, error: null, filter: 'all' };

function reducer(state, action) {
  switch (action.type) {
    case 'fetch-start': return { ...state, loading: true, error: null };
    case 'fetch-success': return { ...state, loading: false, users: action.users };
    case 'fetch-error': return { ...state, loading: false, error: action.error };
    case 'set-filter': return { ...state, filter: action.filter };
    default: return state;
  }
}

Context と Reducer

useReducer と Context の組み合わせは軽量な Redux ライクなストアを作成します。Reducer がロジックを保持し、Context が state と dispatch を配信します。これは中規模アプリのアプリ全体 state の推奨パターンです。

react
function todoReducer(state, action) {
  switch (action.type) {
    case 'add': return [...state, { id: Date.now(), text: action.text, done: false }];
    case 'toggle': return state.map((t) => t.id === action.id ? { ...t, done: !t.done } : t);
    case 'delete': return state.filter((t) => t.id !== action.id);
    default: return state;
  }
}

export function TodoProvider({ children }) {
  const [todos, dispatch] = useReducer(todoReducer, []);
  return <TodoContext.Provider value={{ todos, dispatch }}>{children}</TodoContext.Provider>;
}

アクションタイプとパターン

アクションタイプを定数として定義し、タイプミスを避け IDE オートコンプリートを有効にします。アクション形状 { type, payload? } は一般的な慣習です。TypeScript の場合はアクションタイプの判別共用体を定義します。

react
const ACTIONS = { ADD: 'add', UPDATE: 'update', DELETE: 'delete' };

function reducer(state, action) {
  switch (action.type) {
    case ACTIONS.ADD:
      return [...state, { id: action.id, ...action.payload }];
    case ACTIONS.UPDATE:
      return state.map((item) => item.id === action.id ? { ...item, ...action.payload } : item);
    case ACTIONS.DELETE:
      return state.filter((item) => item.id !== action.id);
    default: return state;
  }
}
21

useMemo と useCallback

useMemo

useMemo は計算結果をキャッシュし、依存関係が変更された時のみ再計算します。高コストな計算(ソート、大きな配列のフィルタ)に使用します。依存配列にはコールバックが使用するすべてのものを含める必要があります。

react
function ProductList({ products, filter }) {
  const filtered = useMemo(() => {
    return products.filter((p) => p.category === filter);
  }, [products, filter]);

  const sorted = useMemo(() => [...filtered].sort((a, b) => a.price - b.price), [filtered]);
  return <ul>{sorted.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}

useCallback

useCallback は関数をメモ化し、依存関係が変更されない限りレンダー間で同じアイデンティティを保持します。これはコールバックをメモ化された子に渡す時に重要です — ないと子が毎回再レンダリングします。

react
function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = useCallback(() => setCount((c) => c + 1), []);
  return <MemoizedChild onClick={handleClick} />;
}

React.memo

React.memo は props が変更された時のみ再レンダリングするようコンポーネントをラップします(浅い比較)。第 2 引数は再レンダリングをスキップするために true を返すカスタムコンパレーターです。props のために memo を useCallback/useMemo と組み合わせます。

react
const ExpensiveItem = React.memo(function ExpensiveItem({ value, onClick }) {
  return <li onClick={onClick}>{value}</li>;
});

// With custom comparison
const DeepChild = React.memo(
  ({ user }) => <div>{user.name}</div>,
  (prev, next) => prev.user.id === next.user.id
);

メモ化するタイミング

メモ化には節約を上回るコストがある可能性があります。以下の場合のみメモ化します:(1) 計算が高コスト、(2) 値がメモ化された子に渡される、(3) 値が useEffect/useMemo の依存関係として使用される。

react
// GOOD: expensive computation
const sorted = useMemo(() => heavySort(data), [data]);

// GOOD: callback passed to memoized child
const onSelect = useCallback((id) => setSelected(id), []);

// BAD: cheap operation, no perf issue
const label = useMemo(() => first + ' ' + last, [first, last]);

参照等価性のための useMemo

useMemo はオブジェクトと配列がレンダー間で同じ参照を保持することを保証し、useEffect/useMemo/useCallback の依存関係として使用される場合に重要です。ないと { q: query } が毎レンダーで新しいオブジェクトを作成します。

react
function Search({ query }) {
  const params = useMemo(() => ({ q: query, limit: 10 }), [query]);
  useEffect(() => {
    api.search(params).then(setData);
  }, [params]); // Without useMemo, this fires every render
}
22

Portals と Refs

createPortal

createPortal は親コンポーネント DOM ツリー外の DOM ノード(通常 document.body)に子をレンダリングします。これは親のスタッキングコンテキストをエスケープする必要のあるモーダル、ツールチップ、ドロップダウンに不可欠です。

react
import { createPortal } from 'react-dom';

function Modal({ open, onClose, children }) {
  if (!open) return null;
  return createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()}>{children}</div>
    </div>,
    document.body
  );
}

useRef

useRef は再レンダリングを引き起こさずにレンダー間で持続する .current を持つミュータブルなオブジェクトを返します。主な用途は DOM ノードへのアクセスです。UI に影響しないミュータブル値も格納します。

react
function FocusInput() {
  const inputRef = useRef(null);
  const focus = () => inputRef.current?.focus();
  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={focus}>Focus</button>
    </>
  );
}

forwardRef

forwardRef は親コンポーネントがラッパーコンポーネントを通じて子 DOM ノードに ref を渡すことを可能にします。ないと React は ref を prop として渡すことを禁止します。ref はラップされた関数の第 2 引数です。

react
const FancyInput = React.forwardRef(function FancyInput({ label, ...props }, ref) {
  return (
    <label>{label}<input ref={ref} {...props} className="fancy-input" /></label>
  );
});

useImperativeHandle

useImperativeHandle は ref 経由で親に公開するインスタンスをカスタマイズします — 生の DOM ノードの代わりに、親は定義したメソッドのみを見ます。控えめに使用し、可能な場合は宣言的 props を優先します。

react
const VideoPlayer = React.forwardRef(function VideoPlayer(props, ref) {
  const videoRef = useRef(null);
  useImperativeHandle(ref, () => ({
    play: () => videoRef.current?.play(),
    pause: () => videoRef.current?.pause(),
    seek: (time) => { if (videoRef.current) videoRef.current.currentTime = time; },
  }));
  return <video ref={videoRef} src={props.src} />;
});

ミュータブル値の Refs

useRef は再レンダリングをトリガーすべきでないミュータブル値を格納します — タイマー ID、WebSocket インスタンス、「is mounted」フラグなど。常に useEffect クリーンアップ関数で副作用をクリーンアップしてください。

react
function Stopwatch() {
  const [seconds, setSeconds] = useState(0);
  const intervalRef = useRef(null);
  const start = () => {
    if (intervalRef.current) return;
    intervalRef.current = setInterval(() => setSeconds((s) => s + 1), 1000);
  };
  const stop = () => { clearInterval(intervalRef.current); intervalRef.current = null; };
  useEffect(() => () => clearInterval(intervalRef.current), []);
  return <>{seconds}<button onClick={start}>Start</button><button onClick={stop}>Stop</button></>;
}
23

エラーバウンダリ

クラスエラーバウンダリ

エラーバウンダリはレンダリング中の子コンポーネントツリーのエラーをキャッチするクラスコンポーネントです。getDerivedStateFromError がフォールバックをレンダリングするための state を更新し、componentDidCatch がエラーをログに記録します。イベントハンドラーや非同期コードのエラーはキャッチしません。

react
class ErrorBoundary extends React.Component {
  constructor(props) { super(props); this.state = { hasError: false, error: null }; }
  static getDerivedStateFromError(error) { return { hasError: true, error }; }
  componentDidCatch(error, info) { console.error('Caught:', error, info); }
  render() {
    if (this.state.hasError) return this.props.fallback || <h1>Something went wrong.</h1>;
    return this.props.children;
  }
}

エラーバウンダリの使用

UI の一部のクラッシュがアプリ全体をダウンさせないよう、エラーバウンダリを戦略的に配置します。ウィジェット周りの細粒度なバウンダリでアプリの残りが動作し続けます。

react
function App() {
  return (
    <ErrorBoundary fallback={<ErrorPage />}>
      <Header />
      <ErrorBoundary fallback={<SidebarCrash />}><Sidebar /></ErrorBoundary>
      <ErrorBoundary fallback={<ContentCrash />}><MainContent /></ErrorBoundary>
    </ErrorBoundary>
  );
}

エラー状態のリセット

エラーバウンダリは state が変更されるまでエラー状態に留まります。hasError を false にリセットする「Try again」ボタンを提供し、React に子を再レンダリングさせます。key prop の変更もリセットします。

react
class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };
  static getDerivedStateFromError(error) { return { hasError: true, error }; }
  reset = () => this.setState({ hasError: false, error: null });
  render() {
    if (this.state.hasError) return <div><p>Failed.</p><button onClick={this.reset}>Try again</button></div>;
    return this.props.children;
  }
}

react-error-boundary ライブラリ

react-error-boundary ライブラリはクラスを書かずに洗練されたフックフレンドリーなエラーバウンダリを提供します。FallbackComponent は error と resetErrorBoundary 関数を受け取ります。resetKeys はそれらの値が変更された時に自動リセットします。

react
import { ErrorBoundary } from 'react-error-boundary';

<ErrorBoundary
  FallbackComponent={ErrorFallback}
  onError={(error, info) => logError(error, info)}
  onReset={() => window.location.reload()}
  resetKeys={[location.pathname]}
>
  <Routes />
</ErrorBoundary>

非同期エラーハンドリング

エラーバウンダリは promise、setTimeout、イベントハンドラーのエラーをキャッチしません。非同期エラーをバウンダリに表示するには、エラーを state に格納しレンダリング中に再スローします。バウンダリがそれをキャッチします。

react
function AsyncComponent() {
  const [state, setState] = useState({ data: null, error: null });
  useEffect(() => {
    let active = true;
    fetchData()
      .then((data) => { if (active) setState({ data, error: null }); })
      .catch((error) => { if (active) setState({ data: null, error }); });
    return () => { active = false; };
  }, []);
  if (state.error) throw state.error;
  if (!state.data) return <Loading />;
  return <div>{state.data}</div>;
}
24

パフォーマンス最適化

仮想化

仮想化は長いリストの表示行のみをレンダリングし、DOM ノードを劇的に削減します。1000 アイテム以上のリストに不可欠です — ないとブラウザが数万ノードでチョークします。

react
import { useVirtualizer } from '@tanstack/react-virtual';

function BigList({ items }) {
  const parentRef = useRef(null);
  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
  });
  return (
    <div ref={parentRef} style={{ height: 600, overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
        {virtualizer.getVirtualItems().map((vi) => (
          <div key={vi.key} style={{ position: 'absolute', top: vi.start, height: vi.size }}>
            {items[vi.index].name}
          </div>
        ))}
      </div>
    </div>
  );
}

コード分割

コード分割はバンドルをオンデマンド読み込みのチャンクに分割します。最もインパクトのある分割はルートレベル(各ページが別チャンク)と重いウィジェット(チャートライブラリ、エディタ)です。バンドルアナライザーで測定します。

react
import { lazy, Suspense } from 'react';
const Admin = lazy(() => import('./Admin'));
const Chart = lazy(() => import('./Chart'));

function Page({ showChart }) {
  return (
    <Suspense fallback={<Skeleton />}>
      {showChart && <Chart data={data} />}
    </Suspense>
  );
}

DevTools でプロファイリング

React DevTools Profiler はレンダリング時間を記録し、どのコンポーネントが再レンダリングされたかとその理由を表示します。props が実際に変更されていない「無駄な」レンダリングを探します — それらが React.memo の候補です。

react
// Use React DevTools Profiler to record renders
// Look for:
// - Components rendering too often
// - Long commit phases
// - Wasted renders (props didn't change)

// Wrap expensive renders to find bottlenecks
function MyComponent({ data }) {
  console.time('render');
  const result = heavyCompute(data);
  console.timeEnd('render');
  return <div>{result}</div>;
}

useDeferredValue

useDeferredValue は値の更新を遅延し、緊急更新(タイピング)を先に実行させます。高コストなレンダリングは遅延値を使用するため、入力をブロックしません。isStale で控えめな視覚ヒントを表示できます。

react
function Search({ query }) {
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;
  const results = useMemo(() => expensiveSearch(deferredQuery), [deferredQuery]);
  return <div style={{ opacity: isStale ? 0.7 : 1 }}>{results.map((r) => <div key={r.id}>{r.name}</div>)}</div>;
}

Concurrent Features

React 18 の concurrent 機能は重い作業中も UI の応答性を保ちます。useTransition と useDeferredValue で React が緊急入力を処理するためにレンダリングを中断できます。自動バッチングが複数の setState 呼び出しを 1 つの再レンダリングにグループ化します。

react
import { useTransition, useDeferredValue } from 'react';

// useTransition: mark updates as non-urgent
const [isPending, startTransition] = useTransition();
const filterResults = (q) => startTransition(() => setResults(search(q)));

// Automatic batching (React 18): state updates in promises batch automatically
fetch('/api').then(() => {
  setLoading(false);   //  \
  setData(data);       //   > single re-render
  setError(null);      //  /
});
25

テスト(React Testing Library)

基本レンダリングとクエリ

render() はフェイク DOM にコンポーネントをマウントし、screen がクエリします。ロールでのクエリ(getByRole)を優先します — 支援技術がページを見る方法を反映し、アクセシビリティを強制します。userEvent が実際のユーザーインタラクションをシミュレートします。

react
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('increments on click', async () => {
  const user = userEvent.setup();
  render(<Counter />);
  expect(screen.getByText('Count: 0')).toBeInTheDocument();
  const button = screen.getByRole('button', { name: /increment/i });
  await user.click(button);
  expect(screen.getByText('Count: 1')).toBeInTheDocument();
});

クエリバリアント

getBy は要素の存在をアサートします(そうでない場合はスロー)。queryBy は不在のアサート用です(null を返す)。findBy は非同期要素の出現を待ちます。All バリアントは複数マッチを処理します。

react
// getBy: throws if 0 or >1 matches (strict)
screen.getByRole('button', { name: 'Submit' });

// queryBy: returns null if 0 matches (for assertions of absence)
expect(screen.queryByText('Error')).not.toBeInTheDocument();

// findBy: returns a Promise, waits for match (async)
const element = await screen.findByText('Loaded');

// getAllBy: returns array (multiple matches)
const items = screen.getAllByRole('listitem');

イベントの発火

userEvent(低レベルの fireEvent ではなく)がインタラクションをシミュレートする推奨方法です — 実際のユーザーが行うすべてのイベント(focus、input、keydown、click)を正しい順序でトリガーします。userEvent メソッドには常に await を使用してください。

react
import userEvent from '@testing-library/user-event';

test('form submission', async () => {
  const user = userEvent.setup();
  const onSubmit = vi.fn();
  render(<Form onSubmit={onSubmit} />);
  await user.type(screen.getByLabelText(/email/i), '[email protected]');
  await user.click(screen.getByRole('button', { name: /submit/i }));
  expect(onSubmit).toHaveBeenCalled();
});

waitFor と非同期

waitFor はアサーションが通過するかタイムアウトするまでポーリングします。findBy* は「これの出現を待つ」一般的なケースの waitFor と getBy の組み合わせです。within はクエリを特定の要素にスコープします。

react
import { waitFor, within } from '@testing-library/react';

test('shows data after fetch', async () => {
  render(<UserList />);
  await waitFor(() => {
    const list = screen.getByRole('list');
    expect(within(list).getAllByRole('listitem')).toHaveLength(3);
  });
});

// findBy is often cleaner than waitFor + getBy
test('shows data (cleaner)', async () => {
  render(<UserList />);
  expect(await screen.findAllByRole('listitem')).toHaveLength(3);
});

モックとセットアップ

MSW(Mock Service Worker)はサービスワーカーレベルでネットワークリクエストをインターセプトし、fetch コードが未変更で実行されるようにします。テストごとにハンドラーをセットアップし、テスト間でリセットし、すべての後にクローズします。

react
import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  rest.get('/api/user', (req, res, ctx) => res(ctx.json({ name: 'Alice' })))
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

test('shows user name', async () => {
  render(<UserProfile />);
  expect(await screen.findByText('Alice')).toBeInTheDocument();
});

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.