Reactで関数のアイデンティティを安定させる「Latest Ref パターン」を理解する
はじめに
useCallback の依存配列にフォームの状態を入れたら、キーストロークのたびに下流のコールバックが全部再生成されて、子コンポーネントまで再レンダリングされる — こんな経験はありませんか?
実務で自動検索機能を実装した際、まさにこの問題に直面しました。解決策として採用したのが 「Latest Ref パターン」 です。React チームが useEvent RFC で公式に解決しようとした問題を、useRef と useCallback の組み合わせで解決するパターンです。
この記事では、なぜこのパターンが必要なのか、何を解決するのか、そして実際のパフォーマンスへの影響はどの程度なのかを、順を追って解説します。
問題: useCallback の依存チェーンが連鎖的に再生成される
useCallback の基本的な動作
useCallback は依存配列(deps)の値が変わると、新しい関数オブジェクトを生成します。
const onSubmit = useCallback(() => {
form.handleSubmit((data) => { search(data); })();
}, [form]); // form が変わるたびに onSubmit は新しい関数になる
ここでの「新しい関数」とは、ロジックが変わるのではなく、メモリ上で別のオブジェクトになるということです。JavaScript では関数もオブジェクトなので、=== で比較すると false になります。
依存チェーンの連鎖
問題は、この onSubmit を他のコールバックが依存している場合に起きます。

フォーム状態が変わると:
onSubmitRawのアイデンティティが変わるonSubmitRawに依存する 4つのコールバック全てが再生成される- それらを props として受け取る子コンポーネントが再レンダリングされる
- React が仮想DOMの差分を計算し、実際には何も変わっていないことを確認して破棄する
キーストロークのたびにこの連鎖が発生します。最終的な結果は同じなのに、React に無駄な仕事をさせています。
解決策: Latest Ref パターン
3ステップの構成
// Step 1: React が追跡しない単一スロット(箱)を作成
const onSubmitRawRef = useRef<ReturnType<typeof form.handleSubmit>>();
// Step 2: 毎レンダリングで箱の中身を最新のクロージャに差し替える
onSubmitRawRef.current = form.handleSubmit((data) => {
// 検索実行ロジック...
});
// Step 3: 安定したラッパー関数(deps が [] なのでアイデンティティ不変)
const onSubmitRaw = useCallback(() => {
onSubmitRawRef.current?.();
}, []);

各ステップの役割
Step 1 では useRef で「箱」を作ります。useRef が返すオブジェクトは、コンポーネントのライフサイクル全体で同一のオブジェクトです。そして重要なのは、.current に何を代入しても React は変更を検知しないことです。useState と違い、再レンダリングをトリガーしません。
Step 2 は毎レンダリングで実行されます。form.handleSubmit(...) は毎回新しいクロージャを生成しますが、それを ref.current に代入するだけです。React の依存追跡には一切影響しません。
Step 3 が外部に公開するラッパー関数です。依存配列が [](空)なので、コンポーネントのライフサイクル全体でアイデンティティが変わりません。呼び出されると ref.current(= 最新のクロージャ)を実行します。
一言でまとめると
中身は常に最新だが、外側の入れ物は React から見て不変。
送信ロジックの変化は内部実装の詳細であり、他のコンポーネントが知る必要がないため、React の追跡から隠しています。
Ref とは何か — よくある誤解
このパターンを理解する上で、useRef の正体を押さえておく必要があります。
Ref は DOM 要素を参照するためだけのものではありません。 useRef は「React が追跡しない単一のミュータブルスロット」を提供するフックです。
// DOM 参照(よく見る使い方)
const inputRef = useRef<HTMLInputElement>(null);
// 値の保持(今回の使い方)
const callbackRef = useRef<() => void>();
重要な特性:
.currentは自由に読み書きできる- 書き換えても再レンダリングは発生しない
- コンポーネントのライフサイクル全体で同一オブジェクト
- 配列やスタックではなく、単一スロット — 常に最新の値1つだけを保持する
Render 1: ref.current = functionV1 // keyphrase="h"
Render 2: ref.current = functionV2 // keyphrase="he" ← V1 は上書きされる
Render 3: ref.current = functionV3 // keyphrase="hel" ← V2 は上書きされる
実際のパフォーマンス影響: 正直なところ
ここで率直に述べると、検索フォーム程度の規模では、ユーザーが体感できるパフォーマンス差はありません。
| 観点 | Ref なし | Ref あり | 体感差 |
|---|---|---|---|
| キーストロークごとのコールバック再生成 | ~5個 | 0個 | なし |
| 子コンポーネントの再レンダリング | 発生しうる | 防止 | ほぼなし |
| メモリ(GC 対象オブジェクト) | 数個多い | 安定 | なし |
| コードの複雑さ | シンプル | やや複雑 | — |
React の仮想 DOM 差分計算は高速なので、数個のコールバック再生成で目に見えるラグは出ません。メモリも数バイト単位の話です。
では、なぜ採用するのか?
- コード衛生: 「意味のない変更を伝搬させない」という原則に沿っている
- スケーラビリティ: もし将来、子コンポーネントが 1000 件のリストをレンダリングするようになったら、この差は実害になる
- 慣例: React コミュニティで広く認知されたパターンであり、同じ問題に対する標準的な解決策
つまり、現時点では予防的な最適化です。しかし、依存チェーンの起点にある関数を安定させるのは、コンポーネントの成長に備えた合理的な判断です。
useEvent RFC との関連
React チームはこの問題を認識しており、RFC #220 で useEvent という Hook を提案しました。
// useEvent が実現すれば、こう書けるはずだった
const onSubmitRaw = useEvent(() => {
form.handleSubmit((data) => { search(data); })();
});
// → アイデンティティ安定 + 常に最新のクロージャを参照
useEvent は Latest Ref パターンを Hook として公式に提供するものです。しかし、本記事執筆時点でもまだ出荷されていません。
そのため、コミュニティでは同等の機能を自前で実装しています:
usehooks-tsのuseLatestCallbackahooksのuseLatest/useMemoizedFn- 各プロジェクトでの手動 Ref パターン(今回の実装)
検索に使えるキーワード:
- "Latest Ref pattern" — パターン名
- "useLatestCallback" — よく見るカスタム Hook 名
- "stable callback ref" — 効果を説明する表現
- "useEvent RFC" — React 公式の取り組み
まとめ
| 要素 | 説明 |
|---|---|
| 問題 | useCallback の deps 変更がチェーン的に下流を再生成する |
| 解決策 | Latest Ref パターン — useRef + useCallback([]) |
| 仕組み | Ref に最新クロージャを格納し、安定したラッパーで参照する |
| パフォーマンス | 小規模では体感差なし。コード衛生とスケーラビリティのため |
| 公式の動き | React useEvent RFC が同じ問題を扱うが未出荷 |
コードレビューでこのパターンの採用を説明する際は、「React の useEvent RFC が解決しようとした問題を、未出荷のため Ref パターンで代替している」 という一文が最も説得力があります。





