It’s now or never

IT系の技術ブログです。気になったこと、勉強したことを備忘録的にまとめて行きます。

useDeferredValueについて理解する

useDeferredValueについて

https://react.dev/reference/react/useDeferredValue#usedeferredvalue

useDeferredValueは、React 18で新しく導入されたhooks。

Suspenseコンポーネントによって、コンポーネントレンダリングに遅延が発生する場合、最新のデータを取得するまでの間、古いデータを表示しておきたいケースなどに使用される。

useDeferredValue が更新された時、まずは裏側で更新後の値でレンダリングを試行する。

遅延なくレンダリングされた場合は、最新の値を反映する。もし、最新の値でのレンダリングが遅延している場合は、そのまま古い値でのレンダリングを継続する。

(値の更新時にそれを使ったコンポーネントがある場合は、完全にレンダリングできるまでは待機してくれるイメージ)

https://react.dev/reference/react/useDeferredValue#how-does-deferring-a-value-work-under-the-hood

こちらを読むに、更新時は古い値でもレンダリングを試行しているとのこと。

ユースケース

  • 最新の情報を取得中に古い情報を見せておく
  • 今の情報が古いことをユーザーに伝える
  • 頻度が高すぎるレンダリングを遅延させパフォーマンスを改善させる

基本的な使い方

React公式のサンプルコードをお借りする。

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>
    </>
  );
}

SearchResults コンポーネントは、遅延レンダリングを含むコンポーネントでqueryが更新されるごとにデータの更新を非同期で行う。

この更新中は、Promiseを待機しているため、Suspenseコンポーネント<h2>Loading...</h2> が呼ばれる。

image

次に、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を更新する。

この時、deferredQueryは、更新後の値を遅延しているため、Suspenseコンポーネント<h2>Loading...</h2>が表示されることはなくなる。

ただし、画面初期化時(空白文字 '' で初回のレンダリング時)については、遅延レンダリングが発生するためLoadingが表示される。

(サンプルコードでは、空白文字時は処理を中断しているのでLoadingは表示されない。)

レンダリングパフォーマンスの改善について

ユースケースのもう一つとしてレンダリングパフォーマンスの改善がある。

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

このようにユーザーのキーストロークごとにリストを更新するケースにおいて、Listのレンダリングコストが比較的高い場合に、UI操作をブロッキングしてしまう問題がある。

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

このような場合に、useDeferredValueを使うと、新しい値を使ってレンダリングが完了されるまでは画面には反映されず、ユーザーがその間新しい操作をした場合は、レンダリングがキャンセルされるためユーザー操作のブロッキングが発生しなくなる。

こちらの更新サンプルをみて実際の挙動を確認するのがわかりやすい。

https://react.dev/reference/react/useDeferredValue#examples

キー入力ごとの処理や、連続した操作によるテクニックとして、DebounceやThrottle がよく使われるが、useDeferredValueで代用できる場合のメリットとしては以下がある。

  • 固定の時間を定義する必要がない
  • レンダリングのキャンセルをReactが制御してくれるのでより、オーバーヘッドが少ない

https://react.dev/reference/react/useDeferredValue#how-is-deferring-a-value-different-from-debouncing-and-throttling