高度なフック
React
useContext・useReducer・useMemo・useCallback
useContext
Context API でグローバル状態を共有
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パターン
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
メモ化で不要な再計算・再生成を防ぐ
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>;
});
// ⚠️ 乱用注意: 単純な値・コンポーネントには不要。
// パフォーマンス問題が実際にある時だけ使う。