useCallback は 再レンダー間で関数定義をキャッシュするための React フックです。子コンポーネントへ渡すコールバック関数を最適化し、不要な再レンダーを防ぐために使用されます。
const cachedFn = useCallback(fn, dependencies)
💡 React Compiler について
React Compiler は値や関数の自動的なメモ化を行うことで、手作業による useCallback 呼び出しの必要性を軽減します。将来的には、コンパイラによる自動メモ化が主流になる可能性があります。
1. なぜ useCallback が必要か
1.1 再レンダーと関数の再生成問題
React では、再レンダーが発生するたびにコンポーネント関数全体が再実行されます。これは関数定義も例外ではありません。
function ProductPage({ productId, referrer, theme }) {
// ❌ 毎回のレンダーで新しい関数が生成される
function handleSubmit(orderDetails) {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}
return (
<div className={theme}>
{/* ShippingForm には毎回違う関数が渡される */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
問題: JavaScript では function() {} や () => {} は常に異なる関数オブジェクトを生成します。これは {} が常に新しいオブジェクトを生成するのと同じです。
// この2つは常に false
console.log(() => {} === () => {}); // false
console.log({} === {}); // false
1.2 memo との組み合わせでの問題
React.memo でラップしたコンポーネントに関数を渡す場合、この問題は致命的になります。
import { memo } from 'react';
// memo でラップして再レンダーを最適化
const ShippingForm = memo(function ShippingForm({ onSubmit }) {
console.log('ShippingForm がレンダーされました');
// ...
});
function ProductPage({ productId, referrer, theme }) {
// ❌ theme が変わるたびに handleSubmit は新しい関数
function handleSubmit(orderDetails) { /* ... */ }
return (
<div className={theme}>
{/* memo の最適化が効かない! */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
theme が変わっただけなのに、ShippingForm は毎回再レンダーされてしまいます。
1.3 useCallback が解決すること
useCallback を使うと、依存値が変化しない限り、同じ関数参照を保持できます:
function ProductPage({ productId, referrer, theme }) {
// ✅ productId か referrer が変わった時だけ新しい関数を生成
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
return (
<div className={theme}>
{/* theme が変わっても handleSubmit は同じ → 再レンダーをスキップ */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
1.4 useCallback の API
const cachedFn = useCallback(fn, dependencies)
| 引数 | 説明 |
|---|---|
fn |
キャッシュしたい関数。任意の引数を取り、任意の値を返せる |
dependencies |
fn 内で参照されるすべてのリアクティブ値の配列 |
返り値:
- 初回レンダー: 渡された
fn関数そのもの - 次回以降: 依存値が変化していなければキャッシュされた関数、変化していれば新しい関数
💡 重要: React は関数を呼び出しません。関数を返すだけです。呼ぶか呼ばないか、いつ呼ぶのかはあなたが決定できます。
1.5 似ているフックとの比較
useCallback と混同しやすいフックがいくつかあります。それぞれの違いを理解しましょう。
useCallback vs useMemo
| 項目 | useCallback |
useMemo |
|---|---|---|
| 目的 | 関数をメモ化 | 値をメモ化 |
| 返り値 | 関数 | 計算結果(任意の型) |
| 使用場面 | 子コンポーネントに渡すコールバック | 高コストな計算結果のキャッシュ |
// useCallback: 関数自体をキャッシュ
const handleClick = useCallback(() => {
console.log(items);
}, [items]);
// useMemo: 計算結果をキャッシュ
const sortedItems = useMemo(() => items.sort(), [items]);
// 💡 実は useCallback は useMemo の糖衣構文!
// useCallback(fn, deps) === useMemo(() => fn, deps)
内部的な等価性:
// この2つは完全に同じ
const handleClick = useCallback(() => {
doSomething(a, b);
}, [a, b]);
const handleClick = useMemo(() => {
return () => {
doSomething(a, b);
};
}, [a, b]);
useCallback vs 通常の関数定義
| 項目 | useCallback |
通常の関数定義 |
|---|---|---|
| 参照の安定性 | 依存値が同じなら同じ参照 | 毎回新しい参照 |
| メモリ | キャッシュ用の追加メモリ | 最小限 |
| 使うべき時 |
memo との組み合わせ、エフェクトの依存値 |
それ以外 |
// ❌ 毎回新しい関数(memo が効かない)
function Parent() {
const handleClick = () => console.log('clicked');
return <MemoizedChild onClick={handleClick} />;
}
// ✅ 同じ関数を再利用(memo が効く)
function Parent() {
const handleClick = useCallback(() => console.log('clicked'), []);
return <MemoizedChild onClick={handleClick} />;
}
useCallback vs useRef
| 項目 | useCallback |
useRef |
|---|---|---|
| 目的 | 関数のメモ化 | 値の永続化 |
| 更新トリガー | 依存値の変化 | なし(手動で更新) |
| 再レンダー | 発生しない | 発生しない |
// useCallback: 依存値が変わると新しい関数
const handleClick = useCallback(() => {
console.log(count);
}, [count]);
// useRef: 常に最新の値を参照可能(関数は再生成されない)
const countRef = useRef(count);
countRef.current = count;
const handleClick = useCallback(() => {
console.log(countRef.current);
}, []); // 依存配列が空!
1.6 重要な注意点
| ルール | 説明 |
|---|---|
| ✅ パフォーマンス最適化のみ |
useCallback を外してもコードが動作すべき |
| ❌ トップレベルでのみ呼び出し | ループや条件分岐内では使用不可 |
| ⚠️ キャッシュは破棄されうる | 特定の状況でReactがキャッシュを破棄することがある |
| ⚠️ すべての関数に使う必要はない |
memo と組み合わせない場合は不要 |
function Component({ items }) {
// ❌ ループ内で呼び出せない
items.map(item => {
const handleClick = useCallback(() => select(item), [item]); // エラー!
});
// ✅ コンポーネントを分離する
return items.map(item => <Item key={item.id} item={item} />);
}
function Item({ item }) {
const handleClick = useCallback(() => select(item), [item]);
return <button onClick={handleClick}>{item.name}</button>;
}
2. useCallback の内部構造を徹底解剖
2.0 全体像: useCallback の処理フロー
🎣 useCallback(フック呼び出し)
↓
📝 初回: mountCallback / 更新: updateCallback
↓
📋 Hook オブジェクトに [関数, 依存配列] を保存
↓
🔍 更新時: areHookInputsEqual で依存配列を比較
↓
✅ 同じなら: キャッシュされた関数を返す
↓
🔄 違うなら: 新しい関数をキャッシュして返す
2.1 エントリポイント: packages/react/src/ReactHooks.js
// packages/react/src/ReactHooks.js
export function useCallback<T>(
callback: T,
deps: Array<mixed> | void | null,
): T {
const dispatcher = resolveDispatcher();
return dispatcher.useCallback(callback, deps);
}
useCallback が呼ばれると、現在のディスパッチャの useCallback メソッドが呼び出されます。ディスパッチャはレンダリングのフェーズ(マウント時 or 更新時)によって異なる実装を持ちます。
2.2 コア実装: mountCallback と updateCallback
初回レンダー時の処理 (mountCallback)
// packages/react-reconciler/src/ReactFiberHooks.js
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}
引用元: packages/react-reconciler/src/ReactFiberHooks.js#L2896-L2901
ポイント:
-
mountWorkInProgressHook()で新しい Hook オブジェクトを作成 -
関数は呼び出されない(
useMemoとの大きな違い!) -
[関数, 依存配列]のタプルをmemoizedStateに保存 - 関数をそのまま返す
更新時の処理 (updateCallback)
// packages/react-reconciler/src/ReactFiberHooks.js
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0]; // ← キャッシュされた関数を返す
}
}
hook.memoizedState = [callback, nextDeps];
return callback; // ← 新しい関数を返す
}
引用元: packages/react-reconciler/src/ReactFiberHooks.js#L2903-L2914
ポイント:
-
updateWorkInProgressHook()で既存の Hook オブジェクトを取得 -
areHookInputsEqualで前回の依存配列と比較 -
依存配列が同じ場合:
prevState[0](キャッシュされた関数)を返す - 依存配列が異なる場合: 新しい関数をキャッシュして返す
2.3 useMemo との比較: 内部実装の違い
useCallback と useMemo の内部実装を比較すると、重要な違いが見えてきます。
// mountCallback: 関数を呼び出さない
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps]; // 関数をそのまま保存
return callback; // 関数をそのまま返す
}
// mountMemo: 関数を呼び出す
function mountMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const nextValue = nextCreate(); // ← 関数を呼び出す!
hook.memoizedState = [nextValue, nextDeps]; // 結果を保存
return nextValue; // 結果を返す
}
| 項目 | mountCallback |
mountMemo |
|---|---|---|
| 保存するもの | 関数自体 | 関数の実行結果 |
| 関数の呼び出し | なし | あり |
| Strict Mode での二重呼び出し | なし | あり |
2.4 依存配列の比較: areHookInputsEqual
// packages/react-reconciler/src/ReactFiberHooks.js
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
): boolean {
if (prevDeps === null) {
return false;
}
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (is(nextDeps[i], prevDeps[i])) { // Object.is で比較
continue;
}
return false;
}
return true;
}
引用元: packages/react-reconciler/src/ReactFiberHooks.js#L454-L500
ポイント:
- 各依存値を
Object.isで比較 -
1つでも異なれば
falseを返し、新しい関数がキャッシュされる - 開発モードでは配列長の変化を警告
2.5 Hook オブジェクトの構造
// packages/react-reconciler/src/ReactFiberHooks.js
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null, // useCallback では [関数, 依存配列]
baseState: null,
baseQueue: null,
queue: null,
next: null, // 次の Hook へのリンク
};
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
Hook のリンクリスト構造:
Fiber.memoizedState
↓
Hook1 → Hook2 → Hook3 → null
(useState) (useCallback) (useEffect)
2.6 内部構造のまとめ
3. ユースケース
3.1 memo でラップしたコンポーネントへの関数渡し
最も一般的なユースケースです。
import { memo, useCallback, useState } from 'react';
// 重いコンポーネントを memo でラップ
const ExpensiveList = memo(function ExpensiveList({
items,
onItemClick
}: {
items: Item[];
onItemClick: (id: string) => void;
}) {
console.log('ExpensiveList rendered');
return (
<ul>
{items.map(item => (
<li key={item.id} onClick={() => onItemClick(item.id)}>
{item.name}
</li>
))}
</ul>
);
});
function App() {
const [items] = useState([/* ... */]);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [theme, setTheme] = useState('light');
// ✅ useCallback で関数をメモ化
const handleItemClick = useCallback((id: string) => {
setSelectedId(id);
}, []); // setSelectedId は安定しているので依存配列に不要
return (
<div className={theme}>
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
{/* theme が変わっても ExpensiveList は再レンダーされない */}
<ExpensiveList items={items} onItemClick={handleItemClick} />
</div>
);
}
3.2 エフェクトの依存値として関数を使う
useEffect の依存配列に関数を含める場合、useCallback が必要になります。
function ChatRoom({ roomId }: { roomId: string }) {
const [messages, setMessages] = useState<Message[]>([]);
// ❌ useCallback なし → エフェクトが毎回実行される
// function createConnection() {
// return new WebSocket(`wss://chat.example.com/${roomId}`);
// }
// ✅ useCallback で関数をメモ化
const createConnection = useCallback(() => {
return new WebSocket(`wss://chat.example.com/${roomId}`);
}, [roomId]);
useEffect(() => {
const connection = createConnection();
connection.onmessage = (event) => {
setMessages(prev => [...prev, JSON.parse(event.data)]);
};
return () => connection.close();
}, [createConnection]); // roomId が変わった時だけ再接続
return <MessageList messages={messages} />;
}
より良い解決策: 関数をエフェクト内に移動することで useCallback を不要にできます。
useEffect(() => {
// ✅ エフェクト内で関数を定義
const connection = new WebSocket(`wss://chat.example.com/${roomId}`);
// ...
}, [roomId]);
3.3 state 更新関数を使った依存値の削減
メモ化したコールバックから state を更新する際、更新関数を使うと依存値を減らせます。
function TodoList() {
const [todos, setTodos] = useState<Todo[]>([]);
// ❌ todos が依存値に含まれる
// const handleAddTodo = useCallback((text: string) => {
// setTodos([...todos, { id: Date.now(), text }]);
// }, [todos]);
// ✅ 更新関数を使って依存値を削減
const handleAddTodo = useCallback((text: string) => {
setTodos(prevTodos => [...prevTodos, { id: Date.now(), text }]);
}, []); // 依存配列が空に!
return (
<>
<AddTodoForm onAdd={handleAddTodo} />
<ul>
{todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
</ul>
</>
);
}
3.4 カスタムフックでの関数メモ化
カスタムフックを作成する際は、返す関数を useCallback でラップすることが推奨されます。
function useRouter() {
const { dispatch } = useContext(RouterContext);
// ✅ カスタムフックから返す関数はメモ化
const navigate = useCallback((url: string) => {
dispatch({ type: 'navigate', url });
}, [dispatch]);
const goBack = useCallback(() => {
dispatch({ type: 'back' });
}, [dispatch]);
return { navigate, goBack };
}
// 使用側
function NavButton() {
const { navigate } = useRouter();
// navigate は安定した参照を持つ
return <button onClick={() => navigate('/home')}>Home</button>;
}
3.5 イベントハンドラの最適化
フォームの入力ハンドラなど、頻繁に呼び出される関数の最適化。
function SearchForm({ onSearch }: { onSearch: (query: string) => void }) {
const [query, setQuery] = useState('');
// ✅ onSearch が変わらない限り、同じ関数を使用
const handleSubmit = useCallback((e: React.FormEvent) => {
e.preventDefault();
onSearch(query);
}, [query, onSearch]);
return (
<form onSubmit={handleSubmit}>
<input
value={query}
onChange={e => setQuery(e.target.value)}
/>
<button type="submit">Search</button>
</form>
);
}
4. アンチパターンと正しい使い方
4.1 不要な useCallback
memo と組み合わせない場合、useCallback は不要です。
// ❌ 意味がない(memo されていないコンポーネントに渡している)
function Parent() {
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return <Child onClick={handleClick} />; // Child は memo されていない
}
// ✅ シンプルに関数を定義
function Parent() {
const handleClick = () => {
console.log('clicked');
};
return <Child onClick={handleClick} />;
}
4.2 依存配列の間違い
// ❌ 依存値が不足
const handleClick = useCallback(() => {
console.log(count); // count が古い値のまま!
}, []);
// ✅ 依存値を正しく指定
const handleClick = useCallback(() => {
console.log(count);
}, [count]);
4.3 オブジェクトや配列を依存値に含める
// ❌ options は毎回新しいオブジェクト
function Component({ options }: { options: Options }) {
const handleClick = useCallback(() => {
doSomething(options);
}, [options]); // 毎回新しい関数に!
}
// ✅ 必要なプロパティだけを依存値に
function Component({ options }: { options: Options }) {
const { mode, timeout } = options;
const handleClick = useCallback(() => {
doSomething({ mode, timeout });
}, [mode, timeout]);
}
5. トラブルシューティング
5.1 useCallback が毎回異なる関数を返す
原因: 依存配列が指定されていない、または依存値が毎回変化している
// ❌ 依存配列なし
const handleClick = useCallback(() => {
doSomething();
}); // 毎回新しい関数!
// ✅ 依存配列を指定
const handleClick = useCallback(() => {
doSomething();
}, []);
デバッグ方法:
const handleClick = useCallback(() => {
doSomething(a, b);
}, [a, b]);
console.log([a, b]); // 依存値をログ出力して変化を確認
5.2 ループ内で useCallback を使いたい
// ❌ ループ内で呼び出せない
function Parent({ items }) {
return (
<>
{items.map(item => {
const handleClick = useCallback(() => { /* ... */ }, [item]); // エラー!
return <Child key={item.id} onClick={handleClick} />;
})}
</>
);
}
// ✅ コンポーネントを分離
function Parent({ items }) {
return (
<>
{items.map(item => (
<ItemWrapper key={item.id} item={item} />
))}
</>
);
}
function ItemWrapper({ item }) {
const handleClick = useCallback(() => { /* ... */ }, [item]);
return <Child onClick={handleClick} />;
}
6. まとめ
useCallback を使うべき場面
| 場面 | 説明 |
|---|---|
memo との組み合わせ |
子コンポーネントの再レンダーを防ぐ |
useEffect の依存値 |
エフェクトの不要な再実行を防ぐ |
| カスタムフックの返り値 | 利用側での最適化を可能にする |
useCallback を使わなくてよい場面
| 場面 | 説明 |
|---|---|
memo なしのコンポーネント |
効果がない |
| 単純なイベントハンドラ | オーバーヘッドが見合わない |
| 計算コストの低い処理 | 最適化の必要がない |
ポイント
useCallback(fn, deps)はuseMemo(() => fn, deps)と等価- 関数は呼び出されない、返されるだけ
- パフォーマンス最適化のみに使用
memoと組み合わせて初めて効果を発揮
// 💡 最終的な使い方のベストプラクティス
const MemoizedChild = memo(Child);
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
return <MemoizedChild onClick={handleClick} />;
}