React

高度なフック

React

useContext・useReducer・useMemo・useCallback

useContext

Context API でグローバル状態を共有

ThemeContext.tsx tsx
import { createContext, useContext, useState, type ReactNode } from 'react';

// 1. Context の型定義と作成
interface ThemeContextType {
    theme:       'light' | 'dark';
    toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | null>(null);

// 2. Provider コンポーネント
export function ThemeProvider({ children }: { children: ReactNode }) {
    const [theme, setTheme] = useState<'light' | 'dark'>('light');
    const toggleTheme = () => setTheme(t => t === 'light' ? 'dark' : 'light');

    return (
        <ThemeContext.Provider value={{ theme, toggleTheme }}>
            {children}
        </ThemeContext.Provider>
    );
}

// 3. カスタムフックでラップ(推奨)
export function useTheme() {
    const ctx = useContext(ThemeContext);
    if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
    return ctx;
}

// 4. 使用
function App() {
    return (
        <ThemeProvider>
            <Header />
        </ThemeProvider>
    );
}

function Header() {
    const { theme, toggleTheme } = useTheme();
    return <button onClick={toggleTheme}>現在: {theme}</button>;
}

useReducer

複雑な状態管理にはreducerパターン

useReducer.tsx tsx
import { useReducer } from 'react';

// Action の型
type Action =
    | { type: 'increment' }
    | { type: 'decrement' }
    | { type: 'reset';     payload: number }
    | { type: 'set';       payload: number };

// Reducer 関数(純粋関数)
function counterReducer(state: number, action: Action): number {
    switch (action.type) {
        case 'increment': return state + 1;
        case 'decrement': return state - 1;
        case 'reset':     return action.payload;
        case 'set':       return action.payload;
        default:          return state;
    }
}

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

    return (
        <div>
            <p>{count}</p>
            <button onClick={() => dispatch({ type: 'increment' })}>+</button>
            <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
            <button onClick={() => dispatch({ type: 'reset', payload: initial })}>Reset</button>
        </div>
    );
}

// useReducer + useContext = 簡易 Redux
export const StateContext  = createContext<State>(initialState);
export const DispatchContext = createContext<Dispatch<Action>>(() => {});

useMemo / useCallback

メモ化で不要な再計算・再生成を防ぐ

memoization.tsx tsx
import { useMemo, useCallback, memo } from 'react';

// useMemo: 重い計算結果をキャッシュ
function ProductList({ products, filter }) {
    // filter が変わった時だけ再計算
    const filtered = useMemo(
        () => products.filter(p => p.category === filter && p.inStock),
        [products, filter]
    );

    // ソートも依存配列で制御
    const sorted = useMemo(
        () => [...filtered].sort((a, b) => a.price - b.price),
        [filtered]
    );

    return <ul>{sorted.map(p => <ProductItem key={p.id} product={p} />)}</ul>;
}

// useCallback: 関数の参照を安定させる
function Parent() {
    const [count, setCount] = useState(0);

    // count が変わっても handleClick の参照が変わらない
    const handleClick = useCallback((id: number) => {
        console.log('clicked:', id, count);
    }, [count]); // count に依存するので含める

    return <Child onClick={handleClick} />;
}

// memo: props が変わらなければ再レンダリングしない
const Child = memo(function Child({ onClick }) {
    console.log('Child rendered');  // useCallback なしだと毎回実行
    return <button onClick={() => onClick(1)}>Click</button>;
});

// ⚠️ 乱用注意: 単純な値・コンポーネントには不要。
// パフォーマンス問題が実際にある時だけ使う。