useDeferredValue

useDeferredValue は、UI の一部の更新を遅延させるための React フックです。

const deferredValue = useDeferredValue(value)

リファレンス

useDeferredValue(value)

コンポーネントのトップレベルで useDeferredValue を呼び出し、その値の遅延されたバージョンを取得します。

import { useState, useDeferredValue } from 'react';

function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// ...
}

さらに例を見る

引数

  • value: 遅延させたい値。任意の型を持つことができます。

返り値

初回レンダー時には、返される値はあなたが渡した値と同一になります。更新時には、React はまず古い値で再レンダーを試み(つまり返り値は古い値になり)、次に新しい値でバックグラウンドで再レンダーを試みます(返り値は更新後の値になります)。

注意点

  • useDeferredValue に渡す値は、プリミティブな値(文字列や数値など)またはレンダーの外部で作成されたオブジェクトであるべきです。レンダー中に新しいオブジェクトを作成してすぐにそれを useDeferredValue に渡すと、それは毎回のレンダーで異なるものとなるため、不必要なバックグラウンドでの再レンダーを引き起こします。

  • useDeferredValue が(Object.is で比較して)異なる値を受け取ると、(前回の値を使用する)現在のレンダーに加えて、新しい値でバックグラウンドで再レンダーをスケジュールします。バックグラウンドでの再レンダーは中断可能です。value に別の更新があると、React はバックグラウンドでの再レンダーを最初からやり直します。例えば、ユーザが素早く入力を行い、それがその値を受け取るチャートコンポーネントが再レンダーできるよりも速かった場合、チャートはユーザがタイプを止めたあとに再表示されることになります。

  • useDeferredValue<Suspense> と統合されています。新しい値によって引き起こされるバックグラウンド更新が UI をサスペンドした場合でも、ユーザにフォールバックは表示されません。データが読み込まれるまで、以前の遅延された値が表示され続けます。

  • useDeferredValue 自体に余計なネットワークリクエストを防ぐ仕組みはありません。

  • useDeferredValue 自体による固定の遅延はありません。React が元の再レンダーを終えるとすぐに、新しい遅延値でのバックグラウンド再レンダー作業を開始します。イベント(タイピングなど)による更新は、バックグラウンドの再レンダーを中断して優先的に処理されます。

  • useDeferredValue によるバックグラウンドの再レンダーは、画面にコミットされるまでエフェクトを実行しません。バックグラウンドの再レンダーがサスペンドする場合、その再レンダーに対応するエフェクトはデータの読み込みと UI の更新の後に実行されます。


使用法

新しいコンテンツが読み込まれている間、古いコンテンツを表示する

UI の一部の更新を遅延させるために、コンポーネントのトップレベルで useDeferredValue を呼び出します。

import { useState, useDeferredValue } from 'react';

function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// ...
}

初回レンダー時には、遅延される値は関数に渡したと同じになります。

更新時には、遅延される値は最新のから「遅れ」ます。具体的には、React はまず遅延値を更新せずに再レンダーを行い、次に新たに受け取った値でバックグラウンドでの再レンダーを試みます。

例を使って、これが役立つ場面を見ていきましょう

補足

この例では、以下のようなサスペンス (Suspense) 対応のデータソースを使用していることを前提としています。

  • RelayNext.js のようなサスペンス対応のフレームワークでのデータフェッチ
  • lazy を用いたコンポーネントコードの遅延ロード
  • use を用いたプロミス (Promise) からの値の読み取り

サスペンスとその制限について詳しく学ぶ。

この例では、SearchResults コンポーネントは検索結果をフェッチする間サスペンドします。"a" を入力し、結果を待ってから "ab" に書き換えてみてください。"a" の検索結果が、ロード中フォールバックに置換されてしまいます。

import { Suspense, useState } from 'react';
import SearchResults from './SearchResults.js';

export default function App() {
  const [query, setQuery] = useState('');
  return (
    <>
      <label>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <SearchResults query={query} />
      </Suspense>
    </>
  );
}

この代わりに一般的に使われる UI パターンは、結果リストの更新を遅延させて、新しい結果が準備できるまで前の結果を表示し続けるというものです。遅延バージョンのクエリ文字列を渡すために useDeferredValue を呼び出します:

export default function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<>
<label>
Search albums:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Loading...</h2>}>
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
}

query の方はすぐに更新されるため、入力フィールドは新しい値を表示します。しかし、deferredQuery はデータが読み込まれるまで前の値を保持するため、SearchResults はしばらく古い結果を表示します。

以下の例で "a" を入力し、結果が読み込まれるのを待ち、次に入力欄を "ab" に書き換えてみてください。新しい結果が読み込まれるまでは、サスペンスによるフォールバックの代わりに古い結果リストが表示され続けることに着目してください。

import { Suspense, useState, useDeferredValue } from 'react';
import SearchResults from './SearchResults.js';

export default function App() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  return (
    <>
      <label>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <SearchResults query={deferredQuery} />
      </Suspense>
    </>
  );
}

さらに深く知る

値の遅延は内部でどのように動作するのか?

値の遅延は 2 つのステップで行われると考えることができます:

  1. まず React は、query は新しい値 ("ab") だが deferredQuery は古い値 ("a") のまま、という状態で再レンダーを試みます。結果リストに渡す側の値である deferredQuery遅延されており、query の値に「遅れて」ついていきます。

  2. バックグラウンドで React は、querydeferredQuery の両方が "ab" に更新された状態で再レンダーを試みます。この再レンダーが完了した場合、React はそれを画面に表示します。しかし、それがサスペンドした("ab" の結果がまだ読み込まれていない)場合、React はこのレンダーの試行を放棄し、データが読み込まれた後にこの再レンダーを再試行します。ユーザは、データが準備できるまで古い遅延された値を見続けます。

遅延された「バックグラウンド」レンダーは中断可能です。例えば、再度入力欄にタイプを行うと、React はそれを放棄し、新しい値でやり直します。React は常に最後に提供された値を使用します。

各キーストロークごとにネットワークリクエストは発生していることに注意してください。ここで(準備ができるまで)遅延させているのは結果の表示であり、ネットワークリクエスト自体ではありません。ユーザが入力を続けた場合でも、各キーストロークのレスポンスはキャッシュされているため、Backspace を押すと即座に反応し、再度のフェッチは起きません。


コンテンツが古いことをインジケータで表示する

上記の例では、最新のクエリの結果リストがまだロード中であることを示すインジケータがありません。新しい結果がロードされるのに時間がかかると、ユーザの混乱を招く可能性があります。結果リストが最新のクエリと一致していないことをユーザに明確に伝えるために、古い結果リストが表示されているときに視覚的なインジケータを追加することができます:

<div style={{
opacity: query !== deferredQuery ? 0.5 : 1,
}}>
<SearchResults query={deferredQuery} />
</div>

これにより、入力を開始すると直ちに、古い結果リストがわずかに暗くなり、新しい結果リストがロードされるまでその状態が続きます。以下の例のように、暗くなるのを遅延させる CSS トランジションを追加することで、徐々に変化するように感じさせることもできます。

import { Suspense, useState, useDeferredValue } from 'react';
import SearchResults from './SearchResults.js';

export default function App() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;
  return (
    <>
      <label>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <div style={{
          opacity: isStale ? 0.5 : 1,
          transition: isStale ? 'opacity 0.2s 0.2s linear' : 'opacity 0s 0s linear'
        }}>
          <SearchResults query={deferredQuery} />
        </div>
      </Suspense>
    </>
  );
}


UI の一部分の再レンダーを遅延させる

useDeferredValue をパフォーマンス最適化として適用することもできます。これは、UI の一部の再レンダーに時間がかかり、それを最適化する簡単な方法がないが、それによって UI の他の部分がブロックされるのを防ぎたい、という場合に有用です。

テキストフィールドと、各キーストロークごとに再レンダーするコンポーネント(チャートや長いリストなど)があると想像してみてください:

function App() {
const [text, setText] = useState('');
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<SlowList text={text} />
</>
);
}

まずは SlowList を最適化して、props が同じ場合は再レンダーをスキップするようにします。これを行うには、memo でラップします

const SlowList = memo(function SlowList({ text }) {
// ...
});

しかし、これが有用なのは SlowList の props が前回のレンダー時と同一である場合のみです。現在直面している問題は、props が異なっており現に別の見た目の結果を表示しないといけない場合に遅い、ということです。

具体的には、主なパフォーマンスの問題は、入力フィールドに何かを入力するたびに、SlowList が新しい props を受け取り、そのツリー全体を再レンダーするため、入力がもたつく感じになるということです。このようなケースでは、useDeferredValue を使うことで、入力フィールドの更新(速くなければならない)を結果リストの更新(遅くても許される)よりも優先することが可能です。

function App() {
const [text, setText] = useState('');
const deferredText = useDeferredValue(text);
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<SlowList text={deferredText} />
</>
);
}

これによって SlowList の再レンダー自体を高速化しているわけではありません。しかし、React に対して、リストの再レンダーは優先度を下げても良いと伝えることで、キーストロークをブロックしないようにします。リストは入力フィールドの「後を追う」形になり、その後「追いつきます」。元と同様に、React はできるだけ早くリストを更新しようとしますが、ユーザの入力をブロックすることはなくなります。

useDeferredValue と最適化されていない再レンダーの違い

1/2:
遅延されたリストの再レンダー

この例では、useDeferredValue が入力をレスポンシブに保つ方法を確認できるよう、SlowList コンポーネントの各アイテムが人為的に遅延させられています。入力フィールドに入力してみて、スムースに入力できる一方で、リストがそれを「追いかける」様子を確認してください。

import { useState, useDeferredValue } from 'react';
import SlowList from './SlowList.js';

export default function App() {
  const [text, setText] = useState('');
  const deferredText = useDeferredValue(text);
  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <SlowList text={deferredText} />
    </>
  );
}

落とし穴

この最適化が動作するには SlowListmemo でラップされていることが必要です。これは、text が変更されるたびに、React が親コンポーネント側を素早く再レンダーできるようにする必要があるからです。その再レンダー中には、deferredText はまだ前の値になっており、SlowList は(props が変更されていないので)再レンダーをスキップできます。memo がなければ、SlowList は常に再レンダーされてしまい、最適化の意味が失われてしまいます。

さらに深く知る

値の遅延とデバウンスやスロットリングとの違い

このようなシナリオにおいて使ったことがあるかもしれない、よくある最適化手法が 2 つあります。

  • デバウンス (debounce) は、ユーザが入力を(例えば 1 秒間)停止するまでリストの更新を待つという意味です。
  • スロットリング (throttling) は、一定の間隔(例えば最大で 1 秒に 1 回)でリストを更新するという意味です。

これらの手法は一部のケースで役立ちますが、useDeferredValue は React 自体と深く統合されており、ユーザのデバイスに適応するため、レンダーの最適化により適しています。

デバウンスやスロットリングとは異なり、遅延される時間を固定で選ぶ必要はありません。ユーザのデバイスが速い場合(例えばパワフルなラップトップ)、遅延された再レンダーはほぼ即座に行われるため、気づかれません。ユーザのデバイスが遅い場合、リストはデバイスの遅さに比例するように入力から「遅れ」ていきます。

また、デバウンスやスロットリングとは異なり、useDeferredValue による遅延された再レンダーはデフォルトで中断可能です。これは、React が大きなリストを再レンダーしている途中で、ユーザが別のキーストロークを行うと、React はその再レンダーを放棄し、キーストロークを処理し、再びバックグラウンドでレンダーをやり直せるという意味です。対照的に、デバウンスやスロットリングの動作はブロッキングであるため、やはり不快な体験を生み出します。それらはレンダーがキーストロークをブロックするタイミングを単に遅らせているに過ぎないのです。

最適化しようとしている作業がレンダーの最中に行われるものでない場合、デバウンスとスロットリングは依然として有用です。例えば、ネットワークリクエストの回数を減らすことができます。これらの手法を一緒に使用することもできます。