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

const deferredValue = useDeferredValue(value)

リファレンス

useDeferredValue(value, initialValue?)

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

import { useState, useDeferredValue } from 'react';

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

以下の例をご覧ください。

パラメーター

  • value: 遅延させたい値。任意の型を使用できます。
  • オプション initialValue: コンポーネントの初期レンダリング中に使用する値。このオプションを省略した場合、useDeferredValue は初期レンダリング中は遅延しません。これは、代わりにレンダリングできる value の以前のバージョンがないためです。

戻り値

  • currentValue: 初回レンダリング中、返される遅延値は initialValue、または指定した値と同じになります。更新中、React は最初に古い値で再レンダリングを試み(したがって、古い値を返します)、次に新しい値でバックグラウンドで別の再レンダリングを試みます(したがって、更新された値を返します)。

注意点

  • 更新がトランジション内にある場合、useDeferredValue は常に新しい value を返し、更新はすでに遅延しているため、遅延レンダリングを生成しません。

  • useDeferredValue に渡す値は、プリミティブ値(文字列や数値など)またはレンダリングの外部で作成されたオブジェクトのいずれかである必要があります。レンダリング中に新しいオブジェクトを作成してすぐに useDeferredValue に渡すと、レンダリングごとに異なるものになり、不必要なバックグラウンドでの再レンダリングが発生します。

  • useDeferredValue が異なる値を受け取った場合(Object.is と比較)、現在のレンダリング(以前の値を使用している場合)に加えて、新しい値でバックグラウンドで再レンダリングをスケジュールします。バックグラウンドでの再レンダリングは中断可能です。つまり、value に別の更新があると、React はバックグラウンドでの再レンダリングを最初から再開します。たとえば、ユーザーが入力した内容をチャートが遅延値を受け取って再レンダリングできるよりも速く入力している場合、チャートはユーザーの入力が止まった後にのみ再レンダリングされます。

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

  • useDeferredValue は、それ自体では追加のネットワークリクエストを防ぎません。

  • useDeferredValue 自体によって引き起こされる固定遅延はありません。React が元の再レンダリングを完了するとすぐに、React は新しい遅延値を使用したバックグラウンドでの再レンダリングを直ちに開始します。(タイピングのような)イベントによって引き起こされた更新は、バックグラウンドでの再レンダリングを中断し、それよりも優先されます。

  • useDeferredValue によって引き起こされるバックグラウンドでの再レンダリングは、画面にコミットされるまで Effect を発火しません。バックグラウンドでの再レンダリングが中断された場合、その Effect はデータが読み込まれて UI が更新された後に実行されます。


使い方

フレッシュなコンテンツのロード中に古いコンテンツを表示する

コンポーネントの最上位レベルで useDeferredValue を呼び出して、UI の一部の更新を遅延させます。

import { useState, useDeferredValue } from 'react';

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

最初のレンダリング中、遅延値 は、提供した と同じになります。

更新中、遅延値 は、最新の よりも「遅れます」。特に、React は最初に遅延値を更新せずに再レンダリングし、次にバックグラウンドで新しく受信した値で再レンダリングしようとします。

これがいつ役立つかを確認するために、例を見ていきましょう。

注意

この例では、Suspense 対応のデータソースを使用することを前提としています

  • RelayNext.js のような Suspense 対応のフレームワークを使用したデータフェッチ
  • lazy を使用したコンポーネントコードの遅延ロード
  • use を使用した Promise の値の読み取り

Suspense とその制限事項について詳しく学んでください。

この例では、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" に編集します。Suspense フォールバックの代わりに、新しい結果がロードされるまで、古い結果リストが表示されることに注目してください。

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:
リストの遅延再レンダリング

この例では、SlowList コンポーネントの各アイテムが意図的に遅くされているため、useDeferredValue で入力が応答性を維持できる様子を確認できます。入力にタイプして、リストが「遅れて」いる間、タイプ入力がすばやく感じられることに注目してください。

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がないと、いずれにしても再レンダリングする必要があり、最適化の目的が損なわれます。

深掘り

値を遅延させることは、デバウンスおよびスロットリングとどう違うのですか?

このシナリオで以前に使用した可能性がある2つの一般的な最適化手法があります。

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

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

デバウンスやスロットリングとは異なり、固定遅延を選択する必要はありません。ユーザーのデバイスが高速な場合(たとえば、高性能なラップトップ)、遅延再レンダリングはほぼ瞬時に行われ、気づきません。ユーザーのデバイスが遅い場合、リストはデバイスの速度に応じて、入力よりも「遅れて」表示されます。

また、デバウンスやスロットリングとは異なり、useDeferredValue によって行われる遅延再レンダリングは、デフォルトで中断可能です。つまり、React が大きなリストの再レンダリング中に、ユーザーが別のキーストロークを行うと、React はその再レンダリングを破棄し、キーストロークを処理してから、バックグラウンドで再レンダリングを再度開始します。対照的に、デバウンスとスロットリングは、レンダリングがキーストロークをブロックする瞬間を単に延期するだけなので、ブロックとなり、ぎこちない体験を生み出します。

最適化している作業がレンダリング中に発生しない場合、デバウンスとスロットリングは依然として役立ちます。たとえば、ネットワークリクエストを減らすことができます。これらの手法を組み合わせて使用することもできます。