[React Hooks] useCallback ガイド
[React Hooks] useCallback ガイド
注: 自分の理解のため Github Copilot (Claude Sonnet 4.5) と壁打ちしながら作成した内容です。 Github Copilot による記述も多く含まれます。
また、本ページでは
useEffectEventについては扱っていません。
目次
- 1. useCallback とは
- 2. useCallback の動作原理
- 3. useCallback を使うべき場面
- 4. useCallback を使うべきでない場面
- 5. 注意点とベストプラクティス
- 6. 実践的な例
- 7. useMemo との違い
- 8. React 19 / React Compiler との関係
- 9. useRef を使った高度なパターン
- 10. まとめ
1. useCallback とは
1.1 概要
useCallback は React の Hooks の一つで、関数をメモ化(memoization)するためのフックです。
メモ化とは:
- 計算結果をキャッシュして再利用する最適化手法
useCallbackの場合、関数の参照を保持し、依存配列が変わらない限り同じ関数インスタンスを返す
主な用途:
- 子コンポーネントへ渡すコールバック関数の最適化
useEffectや他のフックの依存配列に含める関数の安定化- 不要な再レンダリングの防止
1.2 基本構文
const memoizedCallback = useCallback(
() => {
// 関数の処理
doSomething(a, b);
},
[a, b], // 依存配列
);
構成要素:
- コールバック関数 - メモ化したい関数
- 依存配列 - 関数が依存する値のリスト
1.3 依存配列の役割
依存配列は、メモ化された関数がいつ再生成されるかを制御します。
// パターン1: 空の依存配列 → 初回レンダリング時のみ関数を生成
const handleClick = useCallback(() => {
console.log("クリックされました");
}, []);
// パターン2: 特定の値を指定 → その値が変更されたときのみ関数を再生成
const handleClick = useCallback(() => {
console.log(`現在のカウント: ${count}`);
}, [count]);
// パターン3: 複数の値を指定 → いずれかの値が変更されたときに関数を再生成
const handleSubmit = useCallback(() => {
submitForm(userId, formData);
}, [userId, formData]);
1.4 メモ化の仕組み
JavaScript では、関数も参照型です。コンポーネントが再レンダリングされるたびに、関数は新しく作成されます。
function Component() {
// 再レンダリングのたびに新しい関数が作成される
const handleClick = () => {
console.log("クリック");
};
// 同じ処理でも、毎回異なる関数オブジェクト
// handleClick !== 前回のhandleClick
}
useCallback を使うと、依存配列が変わらない限り同じ関数参照を保持できます。
function Component() {
// 依存配列が変わらなければ同じ関数が返される
const handleClick = useCallback(() => {
console.log("クリック");
}, []);
// handleClick === 前回のhandleClick (依存配列が変わらない場合)
}
2. useCallback の動作原理
2.1 関数の再生成とメモリ参照
useCallback なしの場合
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState("");
// 毎回新しい関数が作成される
const handleClick = () => {
console.log("クリック");
};
// nameが変わるだけでParentが再レンダリング
// → handleClickも新しく生成
// → Childも再レンダリング(React.memoを使っていても)
// ※ Childに渡されるhandleClickの参照が変わるため
return <Child onClick={handleClick} />;
}
useCallback ありの場合
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState("");
// 依存配列が空なので、常に同じ関数参照
const handleClick = useCallback(() => {
console.log("クリック");
}, []);
// nameが変わってもhandleClickの参照は同じ
// → React.memoと組み合わせればChildは再レンダリングされない
return <Child onClick={handleClick} />;
}
const Child = React.memo(({ onClick }) => {
console.log("Child rendered");
return <button onClick={onClick}>クリック</button>;
});
2.2 依存配列による更新タイミング
依存配列の値が変更されると、新しい関数が生成されます。
function Component() {
const [count, setCount] = useState(0);
const [multiplier, setMultiplier] = useState(1);
const calculate = useCallback(() => {
return count * multiplier;
}, [count, multiplier]);
// count または multiplier が変更されたときのみ
// calculate は新しい関数として再生成される
}
動作の流れ:
初回レンダリング: count=0, multiplier=1
→ calculate関数を生成
count=1に更新:
→ 依存配列[count, multiplier]が変化
→ calculate関数を再生成
multiplier=2に更新:
→ 依存配列[count, multiplier]が変化
→ calculate関数を再生成
他のstateが変更:
→ 依存配列は変化なし
→ 前回のcalculate関数を再利用
2.3 クロージャと依存配列
useCallback で生成された関数は、生成時の変数の値をクロージャとして保持します。
古い値を参照する問題
function Counter() {
const [count, setCount] = useState(0);
// ❌ 問題: 依存配列にcountがない
const handleClick = useCallback(() => {
console.log(count); // 常に初回の値(0)を参照
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>増加</button>
<button onClick={handleClick}>表示</button>
</div>
);
}
正しい依存配列の指定
function Counter() {
const [count, setCount] = useState(0);
// ✅ 正しい: 依存配列にcountを含める
const handleClick = useCallback(() => {
console.log(count); // 最新のcountを参照
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>増加</button>
<button onClick={handleClick}>表示</button>
</div>
);
}
State の更新関数形式を使う
依存配列を減らしつつ最新の値にアクセスする方法:
function Counter() {
const [count, setCount] = useState(0);
// ✅ より良い: 関数形式の更新で依存配列を空にできる
const handleIncrement = useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={handleIncrement}>増加</button>
</div>
);
}
3. useCallback を使うべき場面
3.1 子コンポーネントへの Props として渡す関数
React.memo でメモ化された子コンポーネントへ関数を渡す場合、useCallback が有効です。
function TodoList() {
const [todos, setTodos] = useState([]);
const [filter, setFilter] = useState("all");
// ✅ useCallbackを使用
const handleToggle = useCallback((id) => {
setTodos((prev) =>
prev.map((todo) =>
todo.id === id ? { ...todo, done: !todo.done } : todo,
),
);
}, []);
// filterが変わってもhandleToggleは再生成されない
// → TodoItemは不要な再レンダリングを回避
return (
<div>
<FilterButtons filter={filter} setFilter={setFilter} />
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} onToggle={handleToggle} />
))}
</div>
);
}
// React.memoでメモ化
const TodoItem = React.memo(({ todo, onToggle }) => {
console.log(`TodoItem ${todo.id} rendered`);
return (
<div>
<input
type="checkbox"
checked={todo.done}
onChange={() => onToggle(todo.id)}
/>
{todo.text}
</div>
);
});
効果:
filterが変わってもhandleToggleの参照は変わらないTodoItemはReact.memoにより、Props が変わらなければ再レンダリングされない- 大量のリストアイテムがある場合、パフォーマンスが向上
3.2 useEffect の依存配列に含める関数
関数を useEffect の依存配列に含める場合、useCallback で安定させるべきです。
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// ✅ useCallbackでメモ化
const fetchUser = useCallback(async () => {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUser(data);
}, [userId]);
useEffect(() => {
fetchUser();
}, [fetchUser]); // fetchUserが変わったときのみ実行
return <div>{user?.name}</div>;
}
補足: このケースでは、
useEffect内で直接関数を定義する方がシンプルなことが多いです。useEffect(() => { async function fetchUser() { const response = await fetch(`/api/users/${userId}`); const data = await response.json(); setUser(data); } fetchUser(); }, [userId]);
useCallback+useEffect依存配列のパターンは、同じ関数をuseEffect以外の場所(ボタンのonClickなど)でも使いたい場合に有用です。
useCallback なしの問題:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// ❌ 毎回新しい関数が生成される
const fetchUser = async () => {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUser(data);
};
useEffect(() => {
fetchUser();
}, [fetchUser]); // 毎回実行される(無限ループの可能性)
return <div>{user?.name}</div>;
}
3.3 useMemo や他のフックの依存配列に含める関数
function DataProcessor({ data }) {
const [threshold, setThreshold] = useState(10);
// ✅ フィルタ関数をメモ化
const filterData = useCallback(
(items) => {
return items.filter((item) => item.value > threshold);
},
[threshold],
);
// filterDataが変わったときのみ再計算
const processedData = useMemo(() => {
const filtered = filterData(data);
return filtered.map((item) => ({
...item,
processed: true,
}));
}, [data, filterData]);
return <DataView data={processedData} />;
}
3.4 カスタムフックから返す関数
カスタムフックから関数を返す場合、useCallback でメモ化するのがベストプラクティスです。
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
});
// ✅ 返す関数をメモ化
const setValue = useCallback(
(value) => {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
},
[key],
);
// ✅ 返す関数をメモ化
const removeValue = useCallback(() => {
setStoredValue(initialValue);
window.localStorage.removeItem(key);
}, [key, initialValue]);
return [storedValue, setValue, removeValue];
}
// 使用側
function Component() {
const [name, setName, removeName] = useLocalStorage("name", "");
// setNameの参照が安定しているため、useEffectの依存配列に安心して含められる
useEffect(() => {
// 何らかの処理
}, [setName]);
}
4. useCallback を使うべきでない場面
4.1 通常のイベントハンドラ
子コンポーネントに渡さない、通常のイベントハンドラには不要です。
// ❌ 不要: 単純なイベントハンドラにuseCallbackは過剰
function Form() {
const [name, setName] = useState("");
const handleChange = useCallback((e) => {
setName(e.target.value);
}, []);
return <input value={name} onChange={handleChange} />;
}
// ✅ これで十分
function Form() {
const [name, setName] = useState("");
const handleChange = (e) => {
setName(e.target.value);
};
return <input value={name} onChange={handleChange} />;
}
理由:
- 関数の再生成コストは非常に小さい
useCallback自体にもオーバーヘッドがある(依存配列の比較処理など)<input>等の DOM 要素はReact.memoでラップされていないため、関数参照が変わっても追加の再レンダリングコストは発生しない
4.2 依存が頻繁に変わる関数
依存配列が頻繁に変わる場合、useCallback の効果が薄れます。
function SearchResults({ query, filters, sortBy, page }) {
// ❌ 依存が多く、頻繁に変わる
const fetchResults = useCallback(async () => {
const response = await fetch(
`/api/search?q=${query}&filters=${filters}&sort=${sortBy}&page=${page}`,
);
return response.json();
}, [query, filters, sortBy, page]);
// この場合、useCallbackの効果は限定的
// 依存が変わるたびに新しい関数が生成される
}
4.3 シンプルな関数
非常にシンプルな関数は、メモ化のコストの方が高い場合があります。
// ❌ 過剰: 単純すぎる関数
function ComponentA() {
const add = useCallback((a, b) => a + b, []);
}
// ✅ これで十分
function ComponentB() {
const add = (a, b) => a + b;
}
4.4 パフォーマンス問題がない場面
計測して問題がない限り、最適化は不要です。
// ❌ 過剰な最適化
function SmallList({ items }) {
const handleClick = useCallback((id) => {
console.log(id);
}, []);
return (
<ul>
{items.map((item) => (
<li key={item.id} onClick={() => handleClick(item.id)}>
{item.name}
</li>
))}
</ul>
);
}
// ✅ シンプルに(アイテムが10個程度なら最適化は不要)
function SmallList({ items }) {
const handleClick = (id) => {
console.log(id);
};
return (
<ul>
{items.map((item) => (
<li key={item.id} onClick={() => handleClick(item.id)}>
{item.name}
</li>
))}
</ul>
);
}
最適化の原則:
- まず動くコードを書く
- パフォーマンス問題を計測する
- 問題がある箇所のみ最適化する
5. 注意点とベストプラクティス
5.1 依存配列の正確な指定
依存配列を正確に指定しないと、古い値を参照するバグが発生します。
// ❌ 問題: queryが依存配列にない
function SearchFormBad() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const handleSearch = useCallback(async () => {
const data = await searchAPI(query); // 初回の空文字列しか参照できない
setResults(data);
}, []);
}
// ✅ 正しい: queryを依存配列に含める
function SearchFormGood() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const handleSearch = useCallback(async () => {
const data = await searchAPI(query);
setResults(data);
}, [query]);
}
5.2 React.memo との組み合わせ
useCallback の効果を最大化するには、子コンポーネントを React.memo でラップします。
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
const handleClick = useCallback(() => {
console.log("クリック");
}, []);
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
{/* textが変わってもChildButton は再レンダリングされない */}
<ChildButton onClick={handleClick} />
</div>
);
}
// ✅ React.memoでラップ
const ChildButton = React.memo(({ onClick }) => {
console.log("ChildButton rendered");
return <button onClick={onClick}>クリック</button>;
});
React.memo なしの場合:
// ❌ React.memoなし
const ChildButton = ({ onClick }) => {
console.log("ChildButton rendered");
return <button onClick={onClick}>クリック</button>;
};
// useCallbackを使っても、親が再レンダリングされると
// 子も再レンダリングされる(React.memoがないため)
5.3 過度な最適化を避ける
すべての関数に useCallback を使うのは過剰です。
// ❌ やりすぎ
function Component() {
const handleClick = useCallback(() => {}, []);
const handleChange = useCallback(() => {}, []);
const handleSubmit = useCallback(() => {}, []);
const handleReset = useCallback(() => {}, []);
// ...
}
最適化のガイドライン:
- デフォルトは最適化しない - 通常の関数として実装
- 計測する - React DevTools Profiler で実際のパフォーマンスを確認
- 必要な箇所のみ最適化 - ボトルネックが特定された場合のみ
useCallbackを追加
5.4 ESLint ルールの活用
eslint-plugin-react-hooks を使うと、依存配列の漏れを検出できます。
// .eslintrc.json
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/exhaustive-deps": "warn"
}
}
例:
function Component() {
const [count, setCount] = useState(0);
// ESLintが警告: countを依存配列に含めるべき
const handleClick = useCallback(() => {
console.log(count);
}, []); // ⚠️ 警告: React Hook useCallback has a missing dependency: 'count'
}
6. 実践的な例
6.1 リスト項目のコールバック
大量のリストアイテムがある場合の最適化:
function TodoApp() {
const [todos, setTodos] = useState([
/* 大量のTodo */
]);
// ✅ 各アクションをメモ化
const handleToggle = useCallback((id) => {
setTodos((prev) =>
prev.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo,
),
);
}, []);
const handleDelete = useCallback((id) => {
setTodos((prev) => prev.filter((todo) => todo.id !== id));
}, []);
const handleEdit = useCallback((id, newText) => {
setTodos((prev) =>
prev.map((todo) => (todo.id === id ? { ...todo, text: newText } : todo)),
);
}, []);
return (
<div>
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
onEdit={handleEdit}
/>
))}
</div>
);
}
// React.memoでメモ化
const TodoItem = React.memo(({ todo, onToggle, onDelete, onEdit }) => {
console.log(`TodoItem ${todo.id} rendered`);
return (
<div>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>削除</button>
<button onClick={() => onEdit(todo.id, prompt("新しいテキスト"))}>
編集
</button>
</div>
);
});
6.2 デバウンス処理との組み合わせ
検索入力などでデバウンスを実装する場合:
基本的な実装
function SearchInput() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
// ✅ デバウンス処理をメモ化(クリーンアップ付き)
const debouncedSearch = useCallback((searchQuery) => {
const timeoutId = setTimeout(async () => {
if (searchQuery) {
const data = await fetch(`/api/search?q=${searchQuery}`).then((r) =>
r.json(),
);
setResults(data);
} else {
setResults([]);
}
}, 500);
// クリーンアップ関数を返す
return () => clearTimeout(timeoutId);
}, []);
useEffect(() => {
const cleanup = debouncedSearch(query);
return cleanup;
}, [query, debouncedSearch]);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="検索..."
/>
<SearchResults results={results} />
</div>
);
}
より実用的な useDebounce カスタムフック
デバウンス処理を再利用可能なカスタムフックとして実装:
// デバウンスされた値を返すフック
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
// 値が変わったらタイマーをセット
const timeoutId = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// クリーンアップでタイマーをクリア
return () => clearTimeout(timeoutId);
}, [value, delay]);
return debouncedValue;
}
// 使用例
function SearchInput() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
// ✅ デバウンスされた検索クエリ
const debouncedQuery = useDebounce(query, 500);
useEffect(() => {
if (debouncedQuery) {
fetch(`/api/search?q=${debouncedQuery}`)
.then((r) => r.json())
.then((data) => setResults(data));
} else {
setResults([]);
}
}, [debouncedQuery]);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="検索..."
/>
<SearchResults results={results} />
</div>
);
}
デバウンスされたコールバックのカスタムフック
関数をデバウンスしたい場合:
function useDebouncedCallback(callback, delay) {
const callbackRef = useRef(callback);
const timeoutRef = useRef(null);
// 最新のコールバックを保持
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
// クリーンアップ
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
// デバウンスされたコールバックを返す
return useCallback(
(...args) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
callbackRef.current(...args);
}, delay);
},
[delay],
);
}
// 使用例
function SearchInput() {
const [results, setResults] = useState([]);
const performSearch = async (searchQuery) => {
if (searchQuery) {
const data = await fetch(`/api/search?q=${searchQuery}`).then((r) =>
r.json(),
);
setResults(data);
}
};
// ✅ デバウンスされた検索関数
const debouncedSearch = useDebouncedCallback(performSearch, 500);
return (
<div>
<input
onChange={(e) => debouncedSearch(e.target.value)}
placeholder="検索..."
/>
<SearchResults results={results} />
</div>
);
}
ポイント:
- クリーンアップ処理: コンポーネントのアンマウント時やクエリ変更時に、保留中のタイマーをクリア
- Ref の活用: 最新のコールバックを参照しつつ、デバウンス関数の参照を安定させる
- 再利用性: カスタムフックにすることで、複数の場所で同じロジックを使える
6.3 カスタムフックでの使用
API フェッチのカスタムフック:
function useApi(endpoint) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// ✅ fetch関数をメモ化
const fetchData = useCallback(
async (params = {}) => {
setLoading(true);
setError(null);
try {
const queryString = new URLSearchParams(params).toString();
const url = queryString ? `${endpoint}?${queryString}` : endpoint;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
return result;
} catch (err) {
setError(err.message);
throw err;
} finally {
setLoading(false);
}
},
[endpoint],
);
// ✅ reset関数もメモ化
const reset = useCallback(() => {
setData(null);
setError(null);
setLoading(false);
}, []);
return { data, loading, error, fetchData, reset };
}
// 使用例
function UserProfile({ userId }) {
const {
data: user,
loading,
error,
fetchData,
} = useApi(`/api/users/${userId}`);
useEffect(() => {
fetchData();
}, [fetchData]); // fetchDataが安定しているため安全
if (loading) return <div>読み込み中...</div>;
if (error) return <div>エラー: {error}</div>;
if (!user) return null;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
<button onClick={() => fetchData()}>再読み込み</button>
</div>
);
}
7. useMemo との違い
7.1 基本的な違い
useCallback:
- 関数そのものをメモ化
- 関数の参照を保持
const memoizedFunction = useCallback(() => {
return a + b;
}, [a, b]);
useMemo:
- 関数の実行結果(値) をメモ化
- 計算結果を保持
const memoizedValue = useMemo(() => {
return a + b;
}, [a, b]);
7.2 使い分けの基準
function Example() {
const [a, setA] = useState(1);
const [b, setB] = useState(2);
// ✅ useCallback: 関数を返したい
const add = useCallback(() => {
return a + b;
}, [a, b]);
// add は関数
const result1 = add(); // 実行が必要
// ✅ useMemo: 値を返したい
const sum = useMemo(() => {
return a + b;
}, [a, b]);
// sum は既に計算された値
const result2 = sum; // そのまま使える
return (
<div>
{/* 関数を子に渡す */}
<Child onCalculate={add} />
{/* 値を表示 */}
<p>合計: {sum}</p>
</div>
);
}
変換の関係:
// これら二つは等価
const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]);
const memoizedCallback = useMemo(() => () => doSomething(a, b), [a, b]);
// useMemoで関数を返すことでuseCallbackと同じ効果
使い分けのポイント:
| 用途 | 使用するフック | 理由 |
|---|---|---|
| コールバック関数をメモ化 | useCallback | 関数の参照を保持するため |
| 高コストな計算結果をメモ化 | useMemo | 計算結果の値を保持するため |
| オブジェクトや配列をメモ化 | useMemo | 値(参照)を保持するため |
| 関数を useEffect の依存配列に含む | useCallback | 意図が明確で可読性が高い |
| JSX のメモ化 | useMemo | JSX(要素)を返すため |
8. React 19 / React Compiler との関係
8.1 React Compiler の概要
React Compiler(以前の React Forget)は、React 19 以降を対象とした別途導入するビルドツール(Babel プラグイン)です。React 19 に同梱されているわけではなく、プロジェクトに個別にインストールして使用します。導入することで、多くの場合で手動の useCallback や useMemo が不要になります。
React Compiler の特徴:
- コンポーネントとフックを自動的に最適化
- 適切な場所に自動的にメモ化を挿入
- 開発者が手動で最適化する必要性を削減
- React 19 以降が必要(React 18 以前では使用不可)
8.2 React Compiler 使用時の useCallback
// React Compiler が有効な場合、これらは自動的に最適化される
function Component() {
const [count, setCount] = useState(0);
// ✅ React Compiler が自動的に最適化
// 手動で useCallback を使う必要がない
const handleClick = () => {
console.log(count);
};
return <ChildComponent onClick={handleClick} />;
}
8.3 手動最適化が依然として必要なケース
React Compiler を使用しても、以下の場合は手動最適化が必要になる可能性があります。
- 外部ライブラリとの統合
- サードパーティライブラリが特定の参照の安定性を要求する場合
- 複雑な最適化ロジック
- カスタムな最適化戦略が必要な場合
- 段階的な移行
- React Compiler を導入する前の既存コードベース
8.4 移行戦略
// 段階的な移行の例
// フェーズ1: 既存のコードベース(useCallback を使用)
function ComponentV1() {
const handleClick = useCallback(() => {
console.log("クリック");
}, []);
return <Button onClick={handleClick} />;
}
// フェーズ2: React Compiler 導入後(シンプルに)
function ComponentV2() {
const handleClick = () => {
console.log("クリック");
};
return <Button onClick={handleClick} />;
}
推奨アプローチ:
- 新規プロジェクト: React Compiler を有効にして、手動最適化は最小限に
- 既存プロジェクト: 段階的に React Compiler を導入し、既存の
useCallbackは動作確認しながら徐々に削除 - パフォーマンス検証: React DevTools Profiler で実際の効果を確認
9. useRef を使った高度なパターン
9.1 最新の値を参照しつつ依存配列を最小化
useRef を活用すると、依存配列を空にしつつ常に最新の値を参照できます。
function Component({ onEvent, data }) {
// ✅ Ref に最新の値を保存
const onEventRef = useRef(onEvent);
const dataRef = useRef(data);
// 常に最新の値で Ref を更新
useEffect(() => {
onEventRef.current = onEvent;
dataRef.current = data;
});
// 依存配列は空だが、常に最新の値を参照できる
const handleClick = useCallback(() => {
onEventRef.current(dataRef.current);
}, []); // 空の依存配列
return <button onClick={handleClick}>クリック</button>;
}
9.2 カスタムフックで抽象化
このパターンをカスタムフックにすると再利用しやすくなります。
// 最新の値を参照する関数を作成するフック
function useLatestCallback(callback) {
const callbackRef = useRef(callback);
useEffect(() => {
callbackRef.current = callback;
});
return useCallback((...args) => {
return callbackRef.current(...args);
}, []);
}
// 使用例
function SearchComponent({ onSearch, filters }) {
// ✅ onSearchやfiltersが変わっても handleSearch の参照は変わらない
const handleSearch = useLatestCallback(() => {
onSearch(filters);
});
// handleSearchの参照が安定しているため、依存配列に安心して含められる
useEffect(() => {
// 何らかの初期化処理
handleSearch();
}, [handleSearch]);
return <button onClick={handleSearch}>検索</button>;
}
9.3 使い分けのガイドライン
| パターン | メリット | デメリット | 使用場面 |
|---|---|---|---|
| useCallback | シンプルで理解しやすい | 依存配列の管理が必要 | 一般的なメモ化 |
| useRef + useCallback | 依存配列を最小化できる | やや複雑 | 頻繁に変わる値を参照する場合 |
| useLatestCallback | 再利用可能で保守性が高い | 追加の抽象化レイヤー | 複数箇所で同じパターンを使う場合 |
10. まとめ
- 関数の参照を安定させるパフォーマンス最適化ツール(依存配列が変わらない限り同じインスタンスを返す)
- 必須ではない(デフォルトは最適化せず、パフォーマンス問題を計測してから使う)
- React Compiler: React 19 以降では自動最適化により手動使用が減少する見込み
- 使うべき場面:
React.memoされた子コンポーネントへ渡す関数useEffectや他のフックの依存配列に含める関数- カスタムフックから返す関数
- 避けるべき場面:
- 通常のイベントハンドラ(子に渡さない場合)
- 依存が頻繁に変わる関数
- 非常にシンプルな関数
- ベストプラクティス:
- 依存配列を正確に指定(ESLint ルールを活用)
React.memoと組み合わせて効果を最大化- 関数形式の state 更新で依存配列を減らす
- 過度な使用は避ける(コードの複雑化を防ぐ)
- 高度な場合は
useRefパターンを検討
// 最適化の順序
// 1. まず動くコードを書く
function Component() {
const handleClick = () => console.log("クリック");
return <button onClick={handleClick}>クリック</button>;
}
// 2. パフォーマンス問題を計測
// 3. 必要な箇所のみ最適化
function Component() {
const handleClick = useCallback(() => console.log("クリック"), []);
return <MemoizedButton onClick={handleClick} />;
}