useDeferredValue は、遅れてもよい値のコピーを返します。値が変わると、Reactはまず古いコピーで画面を描き直して入力を即座に反映させ、その後新しい値で遅い部分を裏でレンダリングします。
単語をすばやく入力してください。入力欄は遅れず、リストは追いついていない間は薄く表示されます。SlowList はReactがレンダリングを始めた時点でログを出すので、コンソールには1文字ごとに SlowList rendering が1行ずつ記録されますが、途中の値のリストが画面に届くことはありません。新しい文字が裏のレンダリングを中断し、Reactが最新のテキストでやり直したからです。query={deferredQuery} を query={query} に変えてもう一度入力すると、キーを押すたびにリストのために0.25秒待たされます。
構文
const deferredValue = useDeferredValue(value);
const deferredValue = useDeferredValue(value, initialValue); // React 19
valueは描画するものなら何でも構いません。入力欄の文字列、数値、オブジェクトなどです。レンダリングのたびに新しいオブジェクトは常に変化と見なされるので、プリミティブが最も適しています。- 戻り値はほとんどの場合
valueと同じです。valueが変わった直後の1回のレンダリングでは前の値を持ったままで、裏のレンダリングで追いつきます。
仕組み
変更のたびに2回のレンダリングが起きます。
- 緊急のレンダリング。新しい
valueと古いdeferredValueで行われます。入力欄は新しいテキストを表示します。deferredValueだけを受け取るコンポーネントは、前回と同じpropsを受け取ります。 - 裏のレンダリング。新しい
deferredValueで行われます。これが遅いほうです。Reactはこれを区切ってレンダリングし、終わる前にvalueがまた変われば放棄して、最新の値でやり直します。
両方の値をログに出せば、これが起きる様子を観察できます。
a のように1文字だけ入力すると、コンソールにさらに2行表示されます。まず deferred = "" と一緒に text = "a"、次に両方とも "a" です。最初のレンダリングが入力を速く保つのですが、それは遅い部分がこのレンダリングを飛ばせる場合だけです。最初の例の SlowList がmemoで包まれているのはそのためです。緊急のレンダリングの間、その query propは変わっていないので、Reactはそれを飛ばします。memo がないと、緊急のレンダリングでも古いテキストで遅いリスト全体が実行され、何も得られません。
古い中身を表示する
裏のレンダリングが進行中の間、value と遅延値は異なります。それらを比べれば、中身を隠さずに、それが古いことをユーザーに伝えられます。
const isStale = query !== deferredQuery;
<div style={{ opacity: isStale ? 0.5 : 1, transition: 'opacity 0.2s' }}>
<SlowList query={deferredQuery} />
</div>
これはたいていスピナーより親切です。古い結果は読めるまま残り、新しい結果の準備ができるとその場で変わります。同じ考え方はSuspenseでも使えます。遅延値によってコンポーネントがデータを読み込む間サスペンドすると、Reactは新しい中身の準備ができるまで、フォールバックではなく古い中身を表示し続けます(Suspenseとlazyを参照)。
初期値(React 19)
デフォルトでは、最初のレンダリングには遅れる対象がないので、遅延値は最初から本当の値です。React 19からは第2引数を渡せます。最初のレンダリングではフックが initialValue を返し、その後Reactが本当の値で裏でもう一度レンダリングします。これにより、軽いものをすぐに表示し、重い部分をその直後に埋められます。
コンソールには2回のレンダリングが表示されます。まず "" で、次に全文でです。第2引数を削除すると、最初から全文でのレンダリングが1回だけ記録されます。
useDeferredValueとuseTransitionの違い
どちらも作業を緊急でないものとして扱い、同じレンダリングの仕組みを使います。違いは置く場所です。
- useTransitionは更新を包みます。
startTransition(() => setQuery(text))と呼ぶので、セッターが必要です。isPendingが無料で手に入ります。 - useDeferredValueは値を包みます。値を使う場所で呼ぶので、値がpropとして届く場合や、自分で制御できないフックから来る場合にも使えます。「保留中」は
value !== deferredValueで自分で計算します。
// You own the state: either works, a transition is direct
startTransition(() => setQuery(nextQuery));
// You only receive the value
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
return <SlowList query={deferredQuery} />;
}
useDeferredValueとデバウンス、スロットルの違い
デバウンスは最後の変更から決まった時間待ち、スロットルは一定の間隔に最大1回だけ更新します。どちらも事前に遅延を決めるので、速いノートパソコンでは必要以上にページを遅くし、遅いスマートフォンでは遅延がまだ短すぎるかもしれません。
useDeferredValue には遅延がありません。裏のレンダリングはすぐに始まるので、速い端末ではリストがほぼ即座に更新され、遅い端末ではReactが入力欄の応答性を保ちつつ、できるだけ頻繁にリストをレンダリングします。裏の作業は中断することもでき、これはデバウンスしたレンダリングにはできないことです。デバウンスしたレンダリングは、一度始まると終わるまで止まります。
できないのは、ネットワークリクエストを減らすことです。新しい値のたびにfetchを始めるなら、完了したレンダリングごとに遅延値はやはり変わるので、リクエストの量が重要ならリクエストそのものをデバウンスしてください。
よくある間違い
memoを忘れる。上で示したとおり、遅いコンポーネントは緊急のレンダリングを飛ばせる必要があります。memoで包まれていないか、ほかのpropのどれかがレンダリングのたびに新しいオブジェクトだと、キーを押すたびに1回ではなく2回レンダリングされます。- 新しいオブジェクトを渡す。
useDeferredValue({ query })はレンダリングのたびに新しいオブジェクトを作るので、遅延値は常に変わったように見え、Reactは毎回余分な裏のレンダリングを始めます。プリミティブを遅延させ(useDeferredValue(query))、そこからオブジェクトを作ってください。 - リクエストが減ると思い込む。遅延値に基づいてデータを取得するコンポーネントは、レンダリングを終えたすべての値についてやはり取得します。
使うべきとき
1つの入力がレンダリングの遅いページの一部を動かし、その遅い部分をメモ化でき、少しの間やや古い結果を表示しても構わないときに useDeferredValue を使ってください。検索結果、フィルターから描き直すチャート、整形したテキストのプレビューなどです。遅い部分が、見えているのは50行なのに5,000行を描画しているといった直せる理由で遅いなら、まずそれを直してください。セッターを自分で持っている場合のこのパターンについては、useTransitionを参照してください。
よくある質問
useDeferredValueは何をするものですか?
本当の値より遅れることのある値を返します。値が変わると、Reactはまず古い遅延値でレンダリングし、その後新しい値で裏でもう一度レンダリングします。その裏のレンダリングは中断できます。
useDeferredValueを使ってもリストが速くならないのはなぜですか?
遅いコンポーネントを memo で包み、遅延値を受け取らせる必要があります。そうしないと、緊急のレンダリングでも古い値で遅いコンポーネントがレンダリングされ、入力は遅いままです。
useDeferredValueとuseTransitionの違いは何ですか?
useTransition はstateの更新を包むので、セッターが必要です。useDeferredValue は値を包むので、propとして受け取る場合や、自分で制御できないフックから来る場合のように、値を受け取るだけのときにも使えます。
useDeferredValueはデバウンスですか?
いいえ。決まった遅延はありません。Reactは緊急のレンダリングの直後に遅延したレンダリングを始め、速い端末では遅れはほとんど見えません。また、それ自体でネットワークリクエストを減らすこともありません。
useDeferredValueの第2引数は何ですか?
React 19から initialValue を渡せます。最初のレンダリングではフックがそれを返し、その後Reactが本当の値で裏でもう一度レンダリングします。