Reactで関数のアイデンティティを安定させる「Latest Ref パターン」を理解する

Reactで関数のアイデンティティを安定させる「Latest Ref パターン」を理解する

ReactのuseCallback依存チェーンがキーストロークのたびに連鎖的に再生成される問題を、useRefとuseCallback([])を組み合わせた「Latest Refパターン」で解決する方法を解説します。
2026.07.12

はじめに

useCallback の依存配列にフォームの状態を入れたら、キーストロークのたびに下流のコールバックが全部再生成されて、子コンポーネントまで再レンダリングされる — こんな経験はありませんか?

実務で自動検索機能を実装した際、まさにこの問題に直面しました。解決策として採用したのが 「Latest Ref パターン」 です。React チームが useEvent RFC で公式に解決しようとした問題を、useRefuseCallback の組み合わせで解決するパターンです。

この記事では、なぜこのパターンが必要なのか、何を解決するのか、そして実際のパフォーマンスへの影響はどの程度なのかを、順を追って解説します。

問題: useCallback の依存チェーンが連鎖的に再生成される

useCallback の基本的な動作

useCallback は依存配列(deps)の値が変わると、新しい関数オブジェクトを生成します。

const onSubmit = useCallback(() => {
  form.handleSubmit((data) => { search(data); })();
}, [form]); // form が変わるたびに onSubmit は新しい関数になる

ここでの「新しい関数」とは、ロジックが変わるのではなく、メモリ上で別のオブジェクトになるということです。JavaScript では関数もオブジェクトなので、=== で比較すると false になります。

依存チェーンの連鎖

問題は、この onSubmit を他のコールバックが依存している場合に起きます。

react-latest-ref-pattern-stable-callback-identity-dependency-chain

フォーム状態が変わると:

  1. onSubmitRaw のアイデンティティが変わる
  2. onSubmitRaw に依存する 4つのコールバック全てが再生成される
  3. それらを props として受け取る子コンポーネントが再レンダリングされる
  4. 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?.();
}, []);

react-latest-ref-pattern-stable-callback-identity-mechanism

各ステップの役割

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 差分計算は高速なので、数個のコールバック再生成で目に見えるラグは出ません。メモリも数バイト単位の話です。

では、なぜ採用するのか?

  1. コード衛生: 「意味のない変更を伝搬させない」という原則に沿っている
  2. スケーラビリティ: もし将来、子コンポーネントが 1000 件のリストをレンダリングするようになったら、この差は実害になる
  3. 慣例: React コミュニティで広く認知されたパターンであり、同じ問題に対する標準的な解決策

つまり、現時点では予防的な最適化です。しかし、依存チェーンの起点にある関数を安定させるのは、コンポーネントの成長に備えた合理的な判断です。

useEvent RFC との関連

React チームはこの問題を認識しており、RFC #220useEvent という Hook を提案しました。

// useEvent が実現すれば、こう書けるはずだった
const onSubmitRaw = useEvent(() => {
  form.handleSubmit((data) => { search(data); })();
});
// → アイデンティティ安定 + 常に最新のクロージャを参照

useEvent は Latest Ref パターンを Hook として公式に提供するものです。しかし、本記事執筆時点でもまだ出荷されていません。

そのため、コミュニティでは同等の機能を自前で実装しています:

  • usehooks-tsuseLatestCallback
  • ahooksuseLatest / 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 パターンで代替している」 という一文が最も説得力があります。

この記事をシェアする

関連記事