その他のフック
その他のフック
Section titled “その他のフック”注意: このドキュメントはTypeScript(TSX)前提で説明しています。現在のReact開発では、TypeScriptの採用が標準となっており、型安全性によるバグの早期発見、IDEの補完機能、リファクタリングの安全性などの理由から、JSXよりもTSXが選定されることが一般的です。
このドキュメントでは、useStateとuseEffect以外の主要なフックを、使用頻度順に説明します。
使用頻度順のフック一覧
Section titled “使用頻度順のフック一覧”- useRef - DOM参照、値を保持(高頻度)
- useContext - Context APIで状態管理(中頻度)
- useMemo - 計算結果のメモ化(中頻度、React Compilerで不要になる可能性)
- useCallback - 関数のメモ化(中頻度、React Compilerで不要になる可能性)
- useReducer - 複雑な状態管理(低頻度、useStateで十分な場合が多い)
1. useRef(高頻度)
Section titled “1. useRef(高頻度)”useRefは、DOM要素への参照を保持する、または再レンダリングを引き起こさない値を保持するためのフックです。
基本的な使い方
Section titled “基本的な使い方”DOM要素への参照
Section titled “DOM要素への参照”import { useRef } from 'react';
function TextInput() { const inputRef = useRef<HTMLInputElement>(null);
const focusInput = () => { // ✅ nullチェックが必要 if (inputRef.current) { inputRef.current.focus(); } };
return ( <div> <input ref={inputRef} type="text" /> <button onClick={focusInput}>Focus Input</button> </div> );}再レンダリングを引き起こさない値を保持
Section titled “再レンダリングを引き起こさない値を保持”function Timer() { const intervalRef = useRef<NodeJS.Timeout | null>(null); const [count, setCount] = useState<number>(0);
const startTimer = () => { intervalRef.current = setInterval(() => { setCount(prev => prev + 1); }, 1000); };
const stopTimer = () => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } };
return ( <div> <p>Count: {count}</p> <button onClick={startTimer}>Start</button> <button onClick={stopTimer}>Stop</button> </div> );}ベストプラクティス
Section titled “ベストプラクティス”- nullチェックを必ず行う:
ref.currentはnullの可能性があるため、必ずチェックする - 型を明示する: TypeScriptでは、DOM要素の型を明示する(
useRef<HTMLInputElement>(null)) - 再レンダリングを引き起こさない値を保持: タイマーID、前回の値など、再レンダリングを引き起こさない値を保持する
アンチパターン
Section titled “アンチパターン”// ❌ 問題のあるコード: ref.currentを直接変更function Component() { const countRef = useRef<number>(0);
const increment = () => { countRef.current += 1; // 再レンダリングされない // 画面に反映されない(再レンダリングが必要な場合はuseStateを使用) };
return <div>{countRef.current}</div>; // 更新されない}
// ✅ 正しいコード: 再レンダリングが必要な場合はuseStateを使用function Component() { const [count, setCount] = useState<number>(0);
const increment = () => { setCount(prev => prev + 1); // 再レンダリングされる };
return <div>{count}</div>; // 更新される}実践例: 前回の値の保持
Section titled “実践例: 前回の値の保持”function usePrevious<T>(value: T): T | undefined { const ref = useRef<T>();
useEffect(() => { ref.current = value; }, [value]);
return ref.current;}
function Component({ count }: { count: number }) { const previousCount = usePrevious(count);
return ( <div> <p>Current: {count}</p> <p>Previous: {previousCount}</p> </div> );}2. useContext(中頻度)
Section titled “2. useContext(中頻度)”useContextは、Contextの値を取得するためのフックです。Context APIと組み合わせて、props drillingを避けるために使用します。
基本的な使い方
Section titled “基本的な使い方”import { createContext, useContext, useState, ReactNode } from 'react';
// Contextの型定義type Theme = 'light' | 'dark';
type ThemeContextType = { theme: Theme; toggleTheme: () => void;};
// Contextの作成const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
// Context Providerfunction ThemeProvider({ children }: { children: ReactNode }) { const [theme, setTheme] = useState<Theme>('light');
const toggleTheme = () => { setTheme(prev => prev === 'light' ? 'dark' : 'light'); };
return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> );}
// カスタムフック: useContextのラッパーfunction useTheme() { const context = useContext(ThemeContext);
if (context === undefined) { throw new Error('useTheme must be used within a ThemeProvider'); }
return context;}
// 使用例function ThemedButton() { const { theme, toggleTheme } = useTheme();
return ( <button onClick={toggleTheme}> Current theme: {theme} </button> );}ベストプラクティス
Section titled “ベストプラクティス”- カスタムフックでラップする:
useContextを直接使わず、カスタムフックでラップしてエラーハンドリングを行う - 型安全性を確保する: Contextの型を明示し、
undefinedチェックを行う - Providerの範囲を最小限に: 必要なコンポーネントのみをProviderで囲む
アンチパターン
Section titled “アンチパターン”// ❌ 問題のあるコード: useContextを直接使用、エラーハンドリングなしfunction ThemedButton() { const context = useContext(ThemeContext); // contextがundefinedの可能性がある return <button>{context.theme}</button>; // エラーが発生する可能性}
// ❌ 問題のあるコード: 過度なContext使用// すべての状態をContextに入れる必要はないconst AppContext = createContext({ user: null, settings: null, theme: null, // ... すべての状態});
// ✅ 正しいコード: 必要な範囲でのみContextを使用// グローバルな状態(テーマ、認証など)のみをContextで管理// ローカルな状態はuseStateで管理実践例: 認証Context
Section titled “実践例: 認証Context”type User = { id: string; name: string; email: string;};
type AuthContextType = { user: User | null; login: (email: string, password: string) => Promise<void>; logout: () => void; isLoading: boolean;};
const AuthContext = createContext<AuthContextType | undefined>(undefined);
function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState<User | null>(null); const [isLoading, setIsLoading] = useState<boolean>(false);
const login = async (email: string, password: string) => { setIsLoading(true); try { // API呼び出し const response = await fetch('/api/login', { method: 'POST', body: JSON.stringify({ email, password }), }); const userData = await response.json(); setUser(userData); } finally { setIsLoading(false); } };
const logout = () => { setUser(null); };
return ( <AuthContext.Provider value={{ user, login, logout, isLoading }}> {children} </AuthContext.Provider> );}
function useAuth() { const context = useContext(AuthContext); if (context === undefined) { throw new Error('useAuth must be used within an AuthProvider'); } return context;}3. useMemo(中頻度)
Section titled “3. useMemo(中頻度)”useMemoは、計算結果をメモ化するためのフックです。ただし、React Compiler(React Forget)が導入されると、手動でのメモ化が不要になる可能性があります。
基本的な使い方
Section titled “基本的な使い方”import { useMemo } from 'react';
type Item = { id: number; value: number;};
function ExpensiveComponent({ items }: { items: Item[] }) { // 高価な計算をメモ化 const total = useMemo(() => { return items.reduce((sum, item) => sum + item.value, 0); }, [items]);
// フィルタリング結果をメモ化 const expensiveItems = useMemo(() => { return items.filter(item => item.value > 1000); }, [items]);
return ( <div> <p>Total: {total}</p> <p>Expensive items: {expensiveItems.length}</p> </div> );}React Compilerとの関係
Section titled “React Compilerとの関係”重要なポイント: React Compiler(React Forget)が導入されると、開発者が手動でuseMemoを書く必要がなくなります。React Compilerが自動的に最適化を行います。
現在のアプローチ(React Compilerなし):
// 開発者が手動でメモ化const expensiveValue = useMemo(() => { return computeExpensiveValue(data);}, [data]);React Compilerのアプローチ:
// React Compilerが自動的に最適化const expensiveValue = computeExpensiveValue(data);ベストプラクティス
Section titled “ベストプラクティス”- 本当に高価な計算のみメモ化: 単純な計算(加算、減算など)はメモ化しない
- 依存配列を正確に指定: ESLintの
exhaustive-depsルールを使用 - 過度なメモ化を避ける: メモ化自体にもコストがかかるため、本当に必要な場合のみ使用
アンチパターン
Section titled “アンチパターン”// ❌ 問題のあるコード: 不要なメモ化function Component({ count }: { count: number }) { // 単純な計算をメモ化する必要はない const doubled = useMemo(() => count * 2, [count]); return <div>{doubled}</div>;}
// ✅ 正しいコード: メモ化が不要な場合は使用しないfunction Component({ count }: { count: number }) { const doubled = count * 2; // メモ化不要 return <div>{doubled}</div>;}
// ❌ 問題のあるコード: 依存配列が不正確function Component({ items, filter }: { items: Item[]; filter: string }) { const filtered = useMemo(() => { return items.filter(item => item.category === filter); }, [items]); // ❌ filterが依存配列に含まれていない return <div>{filtered.length}</div>;}
// ✅ 正しいコード: 依存配列を正確に指定function Component({ items, filter }: { items: Item[]; filter: string }) { const filtered = useMemo(() => { return items.filter(item => item.category === filter); }, [items, filter]); // ✅ すべての依存関係を含める return <div>{filtered.length}</div>;}実践例: ソートとフィルタリング
Section titled “実践例: ソートとフィルタリング”type Product = { id: number; name: string; price: number; category: string;};
function ProductList({ products, sortBy, filterBy }: { products: Product[]; sortBy: 'name' | 'price'; filterBy: string;}) { // フィルタリング結果をメモ化 const filteredProducts = useMemo(() => { if (!filterBy) return products; return products.filter(product => product.category === filterBy ); }, [products, filterBy]);
// ソート結果をメモ化 const sortedProducts = useMemo(() => { return [...filteredProducts].sort((a, b) => { if (sortBy === 'name') { return a.name.localeCompare(b.name); } return a.price - b.price; }); }, [filteredProducts, sortBy]);
return ( <ul> {sortedProducts.map(product => ( <li key={product.id}> {product.name} - {product.price}円 </li> ))} </ul> );}4. useCallback(中頻度)
Section titled “4. useCallback(中頻度)”useCallbackは、関数をメモ化するためのフックです。ただし、React Compiler(React Forget)が導入されると、手動でのメモ化が不要になる可能性があります。
基本的な使い方
Section titled “基本的な使い方”import { useCallback, useState } from 'react';
type Product = { id: number; name: string;};
function Parent() { const [count, setCount] = useState<number>(0);
// 関数をメモ化(依存関係がない場合) const handleClick = useCallback(() => { console.log('Clicked'); }, []);
// 関数をメモ化(依存関係がある場合) const handleProductClick = useCallback((product: Product) => { console.log('Product clicked:', product); setCount(prev => prev + 1); }, []); // setCountは安定しているため、依存配列に含めない
return ( <div> <p>Count: {count}</p> <Child onClick={handleClick} /> <ProductList products={products} onProductClick={handleProductClick} /> </div> );}React Compilerとの関係
Section titled “React Compilerとの関係”重要なポイント: React Compiler(React Forget)が導入されると、開発者が手動でuseCallbackを書く必要がなくなります。React Compilerが自動的に最適化を行います。
現在のアプローチ(React Compilerなし):
// 開発者が手動でメモ化const handleClick = useCallback(() => { // 処理}, []);React Compilerのアプローチ:
// React Compilerが自動的に最適化const handleClick = () => { // 処理};ベストプラクティス
Section titled “ベストプラクティス”- React.memoと組み合わせて使用: 子コンポーネントが
React.memoでメモ化されている場合のみ効果がある - 依存配列を正確に指定: ESLintの
exhaustive-depsルールを使用 - 過度なメモ化を避ける: メモ化自体にもコストがかかるため、本当に必要な場合のみ使用
アンチパターン
Section titled “アンチパターン”// ❌ 問題のあるコード: React.memoなしでuseCallbackを使用function Parent() { const [count, setCount] = useState<number>(0);
const handleClick = useCallback(() => { setCount(prev => prev + 1); }, []);
// ChildがReact.memoでメモ化されていない場合、useCallbackは無意味 return <Child onClick={handleClick} />;}
// ✅ 正しいコード: React.memoと組み合わせて使用const Child = React.memo(function Child({ onClick }: { onClick: () => void }) { return <button onClick={onClick}>Click me</button>;});
function Parent() { const [count, setCount] = useState<number>(0);
const handleClick = useCallback(() => { setCount(prev => prev + 1); }, []);
return <Child onClick={handleClick} />;}
// ❌ 問題のあるコード: 依存配列が不正確function Parent({ userId }: { userId: number }) { const handleClick = useCallback(() => { fetchUser(userId); // userIdを使用しているが、依存配列に含まれていない }, []); // ❌ userIdが依存配列に含まれていない
return <Child onClick={handleClick} />;}
// ✅ 正しいコード: 依存配列を正確に指定function Parent({ userId }: { userId: number }) { const handleClick = useCallback(() => { fetchUser(userId); }, [userId]); // ✅ すべての依存関係を含める
return <Child onClick={handleClick} />;}実践例: フォームハンドラー
Section titled “実践例: フォームハンドラー”import { useCallback, useState } from 'react';
type FormData = { name: string; email: string;};
function Form() { const [formData, setFormData] = useState<FormData>({ name: '', email: '', });
// フォームハンドラーをメモ化 const handleNameChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { setFormData(prev => ({ ...prev, name: e.target.value })); }, []);
const handleEmailChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { setFormData(prev => ({ ...prev, email: e.target.value })); }, []);
const handleSubmit = useCallback((e: React.FormEvent) => { e.preventDefault(); // 送信処理 console.log(formData); }, [formData]);
return ( <form onSubmit={handleSubmit}> <input value={formData.name} onChange={handleNameChange} /> <input value={formData.email} onChange={handleEmailChange} /> <button type="submit">Submit</button> </form> );}5. useReducer(低頻度)
Section titled “5. useReducer(低頻度)”useReducerは、複雑な状態管理に適したフックです。useStateで十分な場合が多いため、使用頻度は低めです。
基本的な使い方
Section titled “基本的な使い方”import { useReducer } from 'react';
// 状態の型定義type State = { count: number; step: number;};
// アクションの型定義type Action = | { type: 'increment' } | { type: 'decrement' } | { type: 'reset' } | { type: 'setStep'; step: number };
// Reducer関数function reducer(state: State, action: Action): State { switch (action.type) { case 'increment': return { ...state, count: state.count + state.step }; case 'decrement': return { ...state, count: state.count - state.step }; case 'reset': return { ...state, count: 0 }; case 'setStep': return { ...state, step: action.step }; default: return state; }}
function Counter() { const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });
return ( <div> <p>Count: {state.count}</p> <p>Step: {state.step}</p> <button onClick={() => dispatch({ type: 'increment' })}>+</button> <button onClick={() => dispatch({ type: 'decrement' })}>-</button> <button onClick={() => dispatch({ type: 'reset' })}>Reset</button> <input type="number" value={state.step} onChange={(e) => dispatch({ type: 'setStep', step: Number(e.target.value) })} /> </div> );}ベストプラクティス
Section titled “ベストプラクティス”- 複雑な状態管理に使用: 複数の状態が関連している場合、または状態の更新ロジックが複雑な場合に使用
- 型安全性を確保する: TypeScriptでActionの型を明示する
- useStateで十分な場合はuseStateを使用: シンプルな状態管理は
useStateで十分
アンチパターン
Section titled “アンチパターン”// ❌ 問題のあるコード: シンプルな状態管理にuseReducerを使用function SimpleCounter() { const [state, dispatch] = useReducer( (state: number, action: 'increment' | 'decrement') => { return action === 'increment' ? state + 1 : state - 1; }, 0 );
return ( <div> <p>Count: {state}</p> <button onClick={() => dispatch('increment')}>+</button> <button onClick={() => dispatch('decrement')}>-</button> </div> );}
// ✅ 正しいコード: シンプルな状態管理はuseStateを使用function SimpleCounter() { const [count, setCount] = useState<number>(0);
return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(prev => prev + 1)}>+</button> <button onClick={() => setCount(prev => prev - 1)}>-</button> </div> );}実践例: フォーム状態管理
Section titled “実践例: フォーム状態管理”type FormState = { name: string; email: string; errors: { name?: string; email?: string; }; isSubmitting: boolean;};
type FormAction = | { type: 'SET_NAME'; name: string } | { type: 'SET_EMAIL'; email: string } | { type: 'SET_ERRORS'; errors: FormState['errors'] } | { type: 'SET_SUBMITTING'; isSubmitting: boolean } | { type: 'RESET' };
function formReducer(state: FormState, action: FormAction): FormState { switch (action.type) { case 'SET_NAME': return { ...state, name: action.name }; case 'SET_EMAIL': return { ...state, email: action.email }; case 'SET_ERRORS': return { ...state, errors: action.errors }; case 'SET_SUBMITTING': return { ...state, isSubmitting: action.isSubmitting }; case 'RESET': return { name: '', email: '', errors: {}, isSubmitting: false, }; default: return state; }}
function Form() { const [state, dispatch] = useReducer(formReducer, { name: '', email: '', errors: {}, isSubmitting: false, });
const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); dispatch({ type: 'SET_SUBMITTING', isSubmitting: true });
// バリデーション const errors: FormState['errors'] = {}; if (!state.name) errors.name = '名前は必須です'; if (!state.email) errors.email = 'メールアドレスは必須です';
if (Object.keys(errors).length > 0) { dispatch({ type: 'SET_ERRORS', errors }); dispatch({ type: 'SET_SUBMITTING', isSubmitting: false }); return; }
// 送信処理 try { await fetch('/api/submit', { method: 'POST', body: JSON.stringify({ name: state.name, email: state.email }), }); dispatch({ type: 'RESET' }); } catch (error) { dispatch({ type: 'SET_ERRORS', errors: { email: '送信に失敗しました' } }); } finally { dispatch({ type: 'SET_SUBMITTING', isSubmitting: false }); } };
return ( <form onSubmit={handleSubmit}> <input value={state.name} onChange={(e) => dispatch({ type: 'SET_NAME', name: e.target.value })} /> {state.errors.name && <span>{state.errors.name}</span>}
<input value={state.email} onChange={(e) => dispatch({ type: 'SET_EMAIL', email: e.target.value })} /> {state.errors.email && <span>{state.errors.email}</span>}
<button type="submit" disabled={state.isSubmitting}> {state.isSubmitting ? '送信中...' : '送信'} </button> </form> );}カスタムフック
Section titled “カスタムフック”カスタムフックは、複数のフックを組み合わせて、再利用可能なロジックを作成するためのパターンです。
基本的な使い方
Section titled “基本的な使い方”import { useState, useCallback } from 'react';
// カスタムフック: useCounterfunction useCounter(initialValue: number = 0) { const [count, setCount] = useState<number>(initialValue);
const increment = useCallback(() => { setCount(prev => prev + 1); }, []);
const decrement = useCallback(() => { setCount(prev => prev - 1); }, []);
const reset = useCallback(() => { setCount(initialValue); }, [initialValue]);
return { count, increment, decrement, reset };}
// 使用例function Counter() { const { count, increment, decrement, reset } = useCounter(0);
return ( <div> <p>Count: {count}</p> <button onClick={increment}>+</button> <button onClick={decrement}>-</button> <button onClick={reset}>Reset</button> </div> );}ベストプラクティス
Section titled “ベストプラクティス”useで始める: カスタムフックの名前はuseで始める- 単一責任の原則: 1つのカスタムフックは1つの責務を持つ
- 型安全性を確保する: TypeScriptで型を明示する
- 再利用性を重視する: 複数のコンポーネントで使えるように設計する
実践例: useLocalStorage
Section titled “実践例: useLocalStorage”function useLocalStorage<T>(key: string, initialValue: T) { const [storedValue, setStoredValue] = useState<T>(() => { try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch (error) { console.error('Error reading from localStorage:', error); return initialValue; } });
const setValue = useCallback((value: T | ((val: T) => T)) => { try { const valueToStore = value instanceof Function ? value(storedValue) : value; setStoredValue(valueToStore); window.localStorage.setItem(key, JSON.stringify(valueToStore)); } catch (error) { console.error('Error saving to localStorage:', error); } }, [key, storedValue]);
return [storedValue, setValue] as const;}
// 使用例function Settings() { const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light');
return ( <div> <p>Current theme: {theme}</p> <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}> Toggle theme </button> </div> );}実践例: useDebounce
Section titled “実践例: useDebounce”function useDebounce<T>(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(value); }, delay);
return () => { clearTimeout(handler); }; }, [value, delay]);
return debouncedValue;}
// 使用例: 検索入力のデバウンスfunction SearchInput() { const [searchTerm, setSearchTerm] = useState<string>(''); const debouncedSearchTerm = useDebounce(searchTerm, 500);
useEffect(() => { if (debouncedSearchTerm) { // API呼び出し fetch(`/api/search?q=${debouncedSearchTerm}`) .then(res => res.json()) .then(data => console.log(data)); } }, [debouncedSearchTerm]);
return ( <input value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} placeholder="Search..." /> );}使用頻度と使い分け
Section titled “使用頻度と使い分け”| フック | 使用頻度 | 主な用途 | 注意点 |
|---|---|---|---|
| useRef | 高 | DOM参照、値を保持 | nullチェック必須 |
| useContext | 中 | グローバル状態管理 | カスタムフックでラップ |
| useMemo | 中 | 計算結果のメモ化 | React Compilerで不要になる可能性 |
| useCallback | 中 | 関数のメモ化 | React Compilerで不要になる可能性 |
| useReducer | 低 | 複雑な状態管理 | useStateで十分な場合が多い |
ベストプラクティスのまとめ
Section titled “ベストプラクティスのまとめ”- 型安全性を確保する: TypeScriptで型を明示する
- 過度な最適化を避ける: 本当に必要な場合のみメモ化する
- 依存配列を正確に指定: ESLintの
exhaustive-depsルールを使用 - カスタムフックでロジックを分離: 再利用性とテストしやすさを向上
- React Compilerの動向を把握: useMemo/useCallbackが不要になる可能性
アンチパターンのまとめ
Section titled “アンチパターンのまとめ”- 不要なメモ化: 単純な計算や関数をメモ化する
- 不正確な依存配列: 依存関係を省略または誤って指定する
- 過度なContext使用: すべての状態をContextに入れる
- useReducerの過度な使用: シンプルな状態管理にuseReducerを使用する
- nullチェックの欠如: useRefでnullチェックを行わない