概要
useCallback は、関数をメモ化 (キャッシュ) するためのReact Hookである。
コンポーネントが再レンダリングされるたびに、コンポーネント内で定義された関数は新しい参照として再生成される。
useCallback を使用すると、依存配列の値が変化しない限り、同じ関数参照を返し続けることができる。
基本構文は useCallback(fn, dependencies) であり、第1引数にメモ化したい関数、第2引数に依存配列を渡す。
依存配列に含まれる値が変化した場合にのみ、新しい関数が作成されて返される。
ただし、useCallback 単体では再レンダリングの最適化は実現しない。
React.memo でラップされた子コンポーネントにpropsとして関数を渡す場合に組み合わせることにより、初めて最適化の効果が生まれる。
子コンポーネントが React.memo でラップされていない場合、useCallback を使用しても再レンダリングの防止には寄与しない。
また、2025年10月にリリースされたReact Compiler v1.0は、ビルド時に自動的にメモ化を適用する機能を持つ。
React Compilerを導入した場合、単純な再レンダリング防止を目的とした useCallback の多くは不要になる。
一方で、useEffect の依存配列に関数を含める場合や、カスタムHookが関数を返す場合には、引き続き useCallback が有効である。
基本構文
useCallback の基本的な構文を以下に示す。
const cachedFn = useCallback(fn, dependencies);
下表に、パラメータの詳細を示す。
| パラメータ | 説明 |
|---|---|
fn |
メモ化したい関数 任意の引数を受け取り、任意の値を返すことができる。 初回レンダリング時にこの関数が返される。 次のレンダリング以降は、依存配列の値が前回と変わっていなければ同じ関数参照が返される。 依存配列の値が変化した場合は、新しい関数が作成されて返される。 |
dependencies |
fn の内部で参照する全てのリアクティブ値のリストprops、state、コンポーネント内で宣言された変数・関数が対象となる。 各値は Object.is() による厳密な比較で評価される。値型は値そのもの、参照型はオブジェクトの参照が比較される。 依存配列を省略すると、毎回新しい関数が作成される。(メモ化されない) 空配列 [] を指定すると、マウント時のみ関数が作成され、以降は同じ関数参照が返される。
|
TypeScriptでの主な型付けパターンを以下に示す。
// パターン1 : 型推論に任せる基本的な形式
const handleSubmit = useCallback((event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
}, []);
// パターン2 : React組み込みのイベントハンドラ型を使用する形式
const handleChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>((event) => {
console.log(event.target.value);
}, []);
// パターン3 : 型エイリアスを定義して使用する形式
type ClickHandler = (id: string) => void;
const handleClick = useCallback<ClickHandler>((id) => {
console.log(id);
}, []);
サンプルコード
子コンポーネントへの関数渡し
useCallback の最も基本的な使用パターンは、React.memo でラップされた子コンポーネントにコールバック関数をpropsとして渡す場合である。
親コンポーネントが再レンダリングされると、コンポーネント内で定義された関数は毎回新しい参照として生成される。
React.memo は参照の同一性でpropsを比較するため、関数の参照が変わるたびに子コンポーネントが不必要に再レンダリングされる。
useCallback で関数参照を安定させることで、この不必要な再レンダリングを防止できる。
// Reactのhooksとmemoをインポート
// memo: コンポーネントのメモ化, useCallback: 関数のメモ化, useState: 状態管理
import { memo, useCallback, useState } from 'react';
// ShippingFormコンポーネントのprops型定義
interface ShippingFormProps {
// 注文送信時のコールバック関数
onSubmit: (orderDetails: { items: string[] }) => void;
}
// React.memoでラップされた子コンポーネント
// propsが変化しない限り再レンダリングされない
const ShippingForm = memo(function ShippingForm({ onSubmit }: ShippingFormProps) {
// フォーム送信ハンドラ
return (
<form onSubmit={(event) => {
event.preventDefault(); // デフォルトのフォーム送信をキャンセル
onSubmit({ items: ['item1'] }); // 親から渡されたコールバックを実行
}}>
<button type="submit">注文を確定する</button>
</form>
);
});
// ProductPageコンポーネントのprops型定義
interface ProductPageProps {
productId: string; // 商品ID
referrer: string; // 参照元情報
}
// 商品ページコンポーネント (親)
export function ProductPage({ productId, referrer }: ProductPageProps) {
// カウンタの状態管理 (ShippingFormとは無関係)
const [count, setCount] = useState(0);
// productIdまたはreferrerが変化した時のみ新しい関数を作成する
// useCallbackにより、関数参照が安定化されShippingFormの不要な再レンダリングを防止
const handleSubmit = useCallback((orderDetails: { items: string[] }) => {
console.log('注文を送信しました:', orderDetails, { productId, referrer });
// 依存配列 : productIdまたはreferrerが変化した場合のみ関数を再生成
}, [productId, referrer]);
// 画面描画
return (
<>
{/* countが変化しても、ShippingFormは再レンダリングされない */}
{/* handleSubmitの参照が変わらないため、memoが効く */}
<ShippingForm onSubmit={handleSubmit} />
{/* カウンタボタン (クリックしてもShippingFormは再レンダリングされない) */}
<button onClick={() => setCount(c => c + 1)}>カウント: {count}</button>
</>
);
}
イベントハンドラの型付け
Reactが提供する組み込みのイベントハンドラ型を使用すると、イベントオブジェクトの型引数を省略して簡潔に記述できる。
// Reactのhooksとmemoをインポート
import { memo, useCallback, useState } from 'react';
// SearchInputコンポーネントのprops型定義
interface SearchInputProps {
// React組み込みのイベントハンドラ型を使用 (型引数にHTMLInputElementを指定)
onChange: React.ChangeEventHandler<HTMLInputElement>; // 値変更イベントハンドラ
onKeyDown: React.KeyboardEventHandler<HTMLInputElement>; // キー押下イベントハンドラ
}
// React.memoでラップされた検索入力コンポーネント
// propsが変化しない限り再レンダリングされない
const SearchInput = memo(function SearchInput({ onChange, onKeyDown }: SearchInputProps) {
return (
<input
type="text"
onChange={onChange} // 値変更時に呼び出される
onKeyDown={onKeyDown} // キー押下時に呼び出される
placeholder="検索キーワードを入力してください"
/>
);
});
// 検索バーコンポーネント (親)
export function SearchBar() {
// 検索クエリの状態管理
const [query, setQuery] = useState('');
// React.ChangeEventHandler<HTMLInputElement> 型を明示する
// 入力値変更時のハンドラ (useCallbackでメモ化)
const handleChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>(
(event) => {
// 入力値をstateに反映
setQuery(event.target.value);
},
// 依存配列が空のため、コンポーネントのライフサイクル中は同じ関数参照が維持される
[]
);
// React.KeyboardEventHandler<HTMLInputElement> 型を明示する
// キー押下時のハンドラ (useCallbackでメモ化)
const handleKeyDown = useCallback<React.KeyboardEventHandler<HTMLInputElement>>(
(event) => {
// Enterキーが押された場合に検索を実行
if (event.key === 'Enter') {
console.log('検索クエリ:', query);
}
},
// queryが変化すると関数を再生成 (最新のquery値をクロージャにキャプチャするため)
[query]
);
// メモ化されたイベントハンドラをpropsとして渡す
return <SearchInput onChange={handleChange} onKeyDown={handleKeyDown} />;
}
カスタムHook内での使用
カスタムHookが関数を返す場合、useCallback でラップすることが推奨される。
カスタムHookを使用するコンポーネントが、返された関数を useEffect の依存配列に含めたり、
React.memo でラップされた子コンポーネントに渡したりする可能性があるためである。
// Reactのhooksをインポート
// useCallback: 関数のメモ化, useState: 状態管理
import { useCallback, useState } from 'react';
// 選択時のコールバック関数の型定義
type OnSelect = (id: string) => void;
// カスタムHookの戻り値の型定義
interface SelectionState {
selectedId: string | null; // 選択中のID (nullの場合は未選択)
handleSelect: OnSelect; // 選択ハンドラ関数
handleClear: () => void; // 選択解除ハンドラ関数
}
// 選択状態を管理するカスタムHook
// useCallbackで関数をメモ化し、使用するコンポーネントでの最適化を可能にする
function useSelection(): SelectionState {
// 選択中のIDを管理するstate
const [selectedId, setSelectedId] = useState<string | null>(null);
// useCallbackでラップして、参照を安定させる
// 依存配列が空のため、コンポーネントのライフサイクル中は同じ関数参照が維持される
const handleSelect = useCallback<OnSelect>((id) => {
setSelectedId(id);
}, []);
// 選択解除ハンドラ (useCallbackでメモ化)
// 依存配列が空のため、コンポーネントのライフサイクル中は同じ関数参照が維持される
const handleClear = useCallback(() => {
setSelectedId(null);
}, []);
// 選択状態とハンドラ関数をオブジェクトとして返す
return { selectedId, handleSelect, handleClear };
}
// カスタムHookを使用するコンポーネント
export function ItemList() {
// カスタムHookから選択状態とハンドラを取得
const { selectedId, handleSelect, handleClear } = useSelection();
// 画面描画
return (
<div>
{/* 現在の選択状態を表示 (nullの場合は「なし」と表示) */}
<p>選択中: {selectedId ?? 'なし'}</p>
{/* Item 1選択ボタン (クリックでhandleSelectを呼び出し) */}
<button onClick={() => handleSelect('item-1')}>Item 1を選択</button>
{/* Item 2選択ボタン (クリックでhandleSelectを呼び出し) */}
<button onClick={() => handleSelect('item-2')}>Item 2を選択</button>
{/* 選択解除ボタン (クリックでhandleClearを呼び出し) */}
<button onClick={handleClear}>選択を解除</button>
</div>
);
}
useEffectとの組み合わせ
関数を useEffect の依存配列に含める必要がある場合、useCallback でメモ化することで無限ループを防止できる。
ただし、関数がコンポーネント外のデータに依存しない場合は、関数を直接 useEffect の内部に移動する方がより適切である。
その場合、useCallback は不要になり、依存配列の管理も簡素化される。
// Reactのhooksをインポート
// useCallback: 関数のメモ化, useEffect: 副作用の管理, useState: 状態管理
import { useCallback, useEffect, useState } from 'react';
// ユーザデータのインターフェース定義
interface UserData {
id: string; // ユーザID
name: string; // ユーザ名
}
// UserProfileコンポーネントのprops型定義
interface UserProfileProps {
userId: string; // 取得対象のユーザID
}
// パターン1 : useCallbackで関数をメモ化してuseEffectの依存配列に含める
export function UserProfileWithCallback({ userId }: UserProfileProps) {
const [user, setUser] = useState<UserData | null>(null); // ユーザデータのstate (取得前はnull)
// ユーザデータを取得する関数 (useCallbackでメモ化)
// userIdが変化した場合のみ関数が再生成される
const fetchUser = useCallback(async () => {
const response = await fetch(`/api/users/${userId}`); // APIからユーザデータを取得
const data = await response.json() as UserData; // レスポンスをJSONとしてパース (UserData型にキャスト)
setUser(data); // stateを更新
// 依存配列: userIdが変化した場合のみ関数を再生成
}, [userId]);
// コンポーネントマウント時またはfetchUserが変化した時に実行
useEffect(() => {
fetchUser();
// fetchUserをuseCallbackでメモ化しているため、userIdが変化した時のみ再実行される
}, [fetchUser]);
// ユーザ名を表示 (取得前は「読み込み中...」を表示)
return <div>{user?.name ?? '読み込み中...'}</div>;
}
// パターン2 (推奨) : 関数をuseEffect内に移動してuseCallbackを不要にする
export function UserProfileWithInlineFunction({ userId }: UserProfileProps) {
const [user, setUser] = useState<UserData | null>(null); // ユーザデータのstate (取得前はnull)
// コンポーネントマウント時またはuserIdが変化した時に実行
useEffect(() => {
// useEffect内に直接定義することで、useCallbackが不要になる
async function fetchUser() {
const response = await fetch(`/api/users/${userId}`); // APIからユーザデータを取得
const data = await response.json() as UserData; // レスポンスをJSONとしてパース (UserData型にキャスト)
setUser(data); // stateを更新
}
// 定義した関数を実行
fetchUser();
// userIdのみを依存配列に指定する (関数を外部で定義する必要がない)
}, [userId]);
// ユーザ名を表示 (取得前は「読み込み中...」を表示)
return <div>{user?.name ?? '読み込み中...'}</div>;
}
React.memoとuseCallbackの組み合わせ
最適化パターンとして、Todoリストを例に React.memo と useCallback の組み合わせを示す。
state更新関数 (アップデータ関数) を使用すると、現在のstateの値を依存配列に含めることなく更新できるため、useCallback の依存配列を最小限に抑えられる。
// Reactのhooksとmemoをインポート
// memo: コンポーネントのメモ化, useCallback: 関数のメモ化, useState: 状態管理
import { memo, useCallback, useState } from 'react';
// Todoアイテムのインターフェース定義
interface Todo {
id: string; // Todoの一意識別子
text: string; // Todoのテキスト内容
}
// TodoItemコンポーネントのprops型定義
interface TodoItemProps {
id: string; // Todo ID
text: string; // Todo テキスト
onDelete: (id: string) => void; // 削除ハンドラ
onUpdate: (id: string, text: string) => void; // 更新ハンドラ
}
// React.memoでラップして、propsが変化しない限り再レンダリングを防止する
// 関数参照がuseCallbackで安定している場合、不要な再レンダリングを回避できる
const TodoItem = memo(function TodoItem({ id, text, onDelete, onUpdate }: TodoItemProps) {
return (
<li>
{/* テキスト入力フィールド (値が変更されるとonUpdateを呼び出し) */}
<input
value={text}
onChange={(e) => onUpdate(id, e.target.value)}
/>
{/* 削除ボタン (クリックでonDeleteを呼び出し) */}
<button onClick={() => onDelete(id)}>削除</button>
</li>
);
});
// Todoリストコンポーネント (親)
export function TodoList() {
const [todos, setTodos] = useState<Todo[]>([]); // Todoの配列を管理するstate
// Todo追加ハンドラ (useCallbackでメモ化)
// 依存配列が空のため、常に同じ関数参照が返される
const handleAddTodo = useCallback(() => {
const newTodo: Todo = { id: crypto.randomUUID(), text: '新しいTodo' }; // 新しいTodoを作成 (crypto.randomUUID()で一意IDを生成)
// アップデーター関数を使用してtodosへの依存をなくす
// prevは現在のstate値を受け取るため、todosを依存配列に含める必要がない
setTodos((prev) => [...prev, newTodo]);
}, []); // 依存配列が空のため、常に同じ関数参照が返される
// Todo削除ハンドラ (useCallbackでメモ化)
// アップデータ関数を使用することにより、todosを依存配列に含めずに済む
const handleDelete = useCallback((id: string) => {
// アップデータ関数でtodosを参照する
// 指定されたID以外のTodoでフィルタリング
setTodos((prev) => prev.filter((todo) => todo.id !== id));
}, []); // todosを依存配列に含めずに済む
// Todo更新ハンドラ (useCallbackでメモ化)
// アップデーター関数を使用することで、todosを依存配列に含めずに済む
const handleUpdate = useCallback((id: string, text: string) => {
// 指定されたIDのTodoのテキストを更新
// mapでTodo配列を走査し、IDが一致する要素のみ更新
setTodos((prev) => prev.map((todo) => todo.id === id ? { ...todo, text } : todo));
}, []); // todosを依存配列に含めずに済む
// 画面描画
return (
<>
{/* Todo追加ボタン */}
<button onClick={handleAddTodo}>Todoを追加</button>
{/* Todoリスト */}
<ul>
{/* 各Todoアイテムを表示 */}
{todos.map((todo) => (
<TodoItem
key={todo.id} // Reactのリストレンダリング用キー
id={todo.id} // Todo ID
text={todo.text} // Todo テキスト
onDelete={handleDelete} // メモ化された削除ハンドラ
onUpdate={handleUpdate} // メモ化された更新ハンドラ
/>
))}
</ul>
</>
);
}
React Compilerとの関係
React Compilerによる自動メモ化
React Compiler (旧称: React Forget) は、2025年10月7日にv1.0としてリリースされた。
React 17以上で動作し、React Nativeにも対応している。
React Compilerはビルド時にReactコードを静的に解析して、必要な箇所に自動的にメモ化を適用する。
これにより、開発者が手動で useCallback や useMemo、React.memo を記述しなくても、同等の最適化が自動で行われる。
さらに、条件付きメモ化等、手動では実装が困難な最適化もコンパイラが判断して適用するため、手動メモ化よりも精度の高い最適化が期待できる。
手動メモ化が必要なケース
React Compilerを導入した後も、以下の場面では useCallback が引き続き有効または必要となる。
useEffectの依存配列に関数を含める場合- 無限ループを防止するために、関数の参照を安定させる必要がある場合
- ただし、可能であれば関数を
useEffect内に移動する方を優先する。
- カスタムHookが関数を返す場合
- カスタムHookの利用者が返された関数をどのように使用するか予測できないため、参照の安定性を保証する目的で
useCallbackを使用する。
- カスタムHookの利用者が返された関数をどのように使用するか予測できないため、参照の安定性を保証する目的で
- React Compilerが適用されない環境との互換性が必要な場合
- React Compilerを導入していないプロジェクトや段階的に移行しているプロジェクトでは引き続き手動メモ化が必要となる。
useCallbackが不要になるケース
React Compilerが導入された環境では、以下に示す用途の useCallback はコンパイラによる自動最適化でカバーされる。
- 単純な再レンダリングを防止するだけの目的
- 親コンポーネントの再レンダリング時に、子コンポーネントへの関数propsの参照を安定させるだけの用途
React.memoと組み合わせた参照安定性の確保- コンパイラが自動的に同等の最適化を適用するため、手動での記述が不要になる。
- 新規コードにおけるほとんどのケース
- React Compiler導入済みの新規プロジェクトでは、コンパイラのデフォルト動作に任せることが推奨される。
useMemoとの違い
useCallback と useMemo は関連するHookであり、以下の等価関係が成立する。
import { useCallback, useMemo } from 'react';
// useCallbackの使用
const cachedFn = useCallback(() => {
doSomething(a, b);
}, [a, b]);
// 上記はuseMemoで以下のように記述することと同等である
const cachedFnWithMemo = useMemo(() => {
return () => {
doSomething(a, b);
};
}, [a, b]);
useCallback(fn, deps) は、useMemo(() => fn, deps) と同等である。
Reactは利便性のために useCallback という専用のHookを提供している。
下表に、両者の使い分けの基準を示す。
| Hook | メモ化の対象 | 主な用途 |
|---|---|---|
useCallback |
関数そのもの | React.memo でラップされた子コンポーネントにpropsとして渡す関数 / useEffect の依存配列に含める関数
|
useMemo |
計算結果 (値) | 重い計算処理の結果 / フィルタリングや変換処理の結果 / 参照の安定性が必要なオブジェクトや配列 |
注意事項
useCallbackだけでは最適化にならない
useCallback 単体では、子コンポーネントの再レンダリングを防止できない。
子コンポーネントが React.memo でラップされていない場合、親コンポーネントが再レンダリングされると関数の参照に関係なく子コンポーネントも再レンダリングされる。
import { useCallback, useState } from 'react';
// React.memoでラップされていないコンポーネント
function ChildComponent({ onClick }: { onClick: () => void }) {
console.log('ChildComponentがレンダリングされた');
return <button onClick={onClick}>クリック</button>;
}
export function ParentComponent() {
const [count, setCount] = useState(0);
// useCallbackを使っているが、子コンポーネントがmemoでラップされていないため効果がない
const handleClick = useCallback(() => {
console.log('クリックされた');
}, []);
return (
<>
{/* countが変化するたびに、ChildComponentも再レンダリングされてしまう */}
<ChildComponent onClick={handleClick} />
<button onClick={() => setCount(c => c + 1)}>カウント: {count}</button>
</>
);
}
クロージャの古い値を参照する問題
依存配列に必要な値を含め忘れると、関数が古いクロージャの値を参照し続けるバグが発生する。
import { useCallback, useState } from 'react';
export function CounterWithBug() {
const [count, setCount] = useState(0);
// 問題 : countが依存配列に含まれていないため、常に初期値の0を参照する
const handleAlertBad = useCallback(() => {
alert(`現在のカウント: ${count}`); // 常に0が表示される
}, []); // countが含まれていない
// 解決策1 : countを依存配列に含める
const handleAlertGood = useCallback(() => {
alert(`現在のカウント: ${count}`);
}, [count]); // countが変化するたびに新しい関数が作成される
// 解決策2 : state更新関数を使用して依存配列を回避する
const handleIncrement = useCallback(() => {
setCount(c => c + 1); // countへの依存なしにstateを更新できる
}, []); // 依存配列が空で済む
return (
<div>
<p>カウント: {count}</p>
<button onClick={handleIncrement}>+1</button>
<button onClick={handleAlertBad}>バグあり (カウントを表示)</button>
<button onClick={handleAlertGood}>正しい (カウントを表示)</button>
</div>
);
}
依存配列の不適切な指定
依存配列にオブジェクトリテラルや配列リテラルを直接含めると、レンダリングのたびに新しい参照が生成されるため、useCallback のメモ化が機能しない。
import { useCallback } from 'react';
interface Config {
theme: string;
size: number;
}
interface ComponentProps {
config: Config;
}
export function Component({ config }: ComponentProps) {
// 問題 : configはレンダリングのたびに新しい参照になる可能性があるため、
// useCallbackは毎回新しい関数を作成してしまう
const handleActionBad = useCallback(() => {
console.log(config.theme, config.size);
}, [config]); // configが参照型の場合、毎回新しい参照になりうる
// 解決策 : オブジェクトのプリミティブ値を個別に依存配列に含める
const handleActionGood = useCallback(() => {
console.log(config.theme, config.size);
}, [config.theme, config.size]); // プリミティブ値で比較される
return <button onClick={handleActionGood}>実行</button>;
}
useCallbackを使うべきでない場面
以下に示す場面では、useCallback を使用しても効果がなく、ソースコードの複雑さが増すだけとなる。
- 関数をメモ化するだけで、子コンポーネントが
React.memoでラップされていない場合- メモ化した関数を受け取る子コンポーネントが最適化されていなければ、
useCallbackの効果は得られない。
- メモ化した関数を受け取る子コンポーネントが最適化されていなければ、
- 単純なイベントハンドラで、子コンポーネントの最適化が不要な場合
- フォームのボタン等、最適化が不要なシンプルな用途では
useCallbackは不要である。
- フォームのボタン等、最適化が不要なシンプルな用途では
- 関数が軽量な処理しか行わない場合
- メモ化のオーバーヘッドが最適化の効果を上回る可能性がある。
- React Compilerを導入済みのプロジェクトで、新規作成する再レンダリング防止目的のコード
- コンパイラが自動的に最適化を適用するため、手動でのメモ化は不要である。
関連情報
- useCallback - React公式ドキュメント
- useMemo - React公式ドキュメント
- React Compiler - React公式ドキュメント
- Reactの基礎 - Hooksの基礎
- Reactの基礎 - useState
- Reactの基礎 - useEffect
- Reactの基礎 - useMemo
- Reactの基礎 - カスタムHook