React useDeferredValueでdebounceなしに検索を最適化する

問題

検索フィルター付きのリストを作りましたが、データが多いとキーストロークのたびに再レンダリングが走り、入力がカクつきます。debounceで対応はできますが、入力中に値が見えない固定の遅延が気になります。

function SearchList({ items }) {
  const [query, setQuery] = useState('');

  // itemsが10000件あると、毎回のタイピングで全件フィルタリング
  const filtered = items.filter(item =>
    item.name.toLowerCase().includes(query.toLowerCase())
  );

  return (
    <>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <List items={filtered} />
    </>
  );
}

解決方法

useDeferredValueを使えば、入力は即座に反映しつつ、重いレンダリングは後回しにできます。

import { useState, useDeferredValue, memo } from 'react';

function SearchList({ items }) {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);

  // 入力はqueryで即時反映、リストはdeferredQueryで遅延レンダリング
  const filtered = items.filter(item =>
    item.name.toLowerCase().includes(deferredQuery.toLowerCase())
  );

  const isStale = query !== deferredQuery;

  return (
    <>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <div style=>
        <SlowList items={filtered} />
      </div>
    </>
  );
}

// memoで囲まないと遅延効果が正しく動作しません
const SlowList = memo(function SlowList({ items }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
});

debounceとの違いはこうです:

debounce: 入力停止 → 300ms待機 → フィルタリング開始
useDeferredValue: 入力は即時反映 → ブラウザの空き時間にフィルタリング更新

ユーザーの入力は一切遅延せず、重い再レンダリングだけをReactが自動的に後回しにします。

ポイント

  • useDeferredValueは緊急な更新(入力)と非緊急な更新(リスト描画)を分離します
  • memoと一緒に使う必要があります — 使わないと毎回再レンダリングされてしまいます
  • debounceと違い固定の遅延がないので、高速なデバイスではほぼ即時に反映され、低速なデバイスでのみ遅延が発生します