C
React/最適化/Lesson 18

useTransition / useDeferredValue — 重い更新をバックグラウンドへ

30分·theory
このチャプター
1/2
TypeScript

useTransition / useDeferredValue — 重い更新をバックグラウンドへ

💡 なぜ学ぶのか? — 入力がカクつく理由

🎯 検索ボックスで素早くタイピングすると入力が詰まる現象 — 値の変更が重いフィルタリング・レンダリングと同じ優先度で処理されるため。
💼 useTransition は「この state の更新は急がない — 入力の方が重要だ」と React に伝えます。
React は入力の更新を先に表示し、重い更新はバックグラウンドに譲渡します → 入力がスムーズになります。
🔗 useDeferredValue は同様の効果を「値」単位で実現します — 遅延された値だけを重い子コンポーネントに渡すパターンです。
📈 かつてデバウンスやスロットルで対処していた UX の問題を、React がタイムスライシングで解決します。
🏢 실무에서는
検索オートコンプリート、大きなリストのフィルタリング、チャートデータの変更、重いマークダウンプレビュー — これらはすべて useTransition の領域です。ユーザーは「入力がスムーズだ」とだけ感じ、重い処理はバックグラウンドで自動的に描画されます。

useTransition · useDeferredValue · startTransition

1. useTransition — 「この setState は後回し可能」と示す

tsx
const [isPending, startTransition] = useTransition();

startTransition(() => {
  setResults(filter(query)); // 「後回し可能」
});
  • startTransition(fn) 内の setState は後回し可能としてマークされます。
  • React はより緊急な更新(入力・クリック)があれば、そちらを先に処理します。
  • isPending で後回しにされた更新が進行中かどうかを確認 → スピナーを表示。

2. useDeferredValue — 値を「一拍遅れのコピー」として扱う

tsx
const deferredQuery = useDeferredValue(query); // 一拍遅れてついてくる

<HeavyResults query={deferredQuery} />
  • query は即時更新(入力が滑らか)。
  • deferredQuery は React に余裕があるときに追従します。
  • HeavyResults が deferredQuery で重い処理をしても、入力には影響しません。

3. いつどちらを使う?

状況推奨
自分が setState の呼び出し元(直接制御)useTransition
受け取った値を子に渡す際、その子が重いuseDeferredValue
後回し中の状態(isPending)が必要useTransition
値の「最新版」と「一拍遅れ版」を同時に表現useDeferredValue

4. startTransition — フック不要でも呼び出せる

ts
import { startTransition } from 'react';

button.addEventListener('click', () => {
  startTransition(() => setData(newData));
});
// isPending の追跡はできず、単に「後回し可能」とマークするだけ

5. 注意点

  • 処理が本当に重い場合(1秒以上)は useTransition だけでは不十分。Web Worker などが必要になります。
  • fetch 自体は後回しにできません — fetch のレスポンスを処理する setState のみ後回し可能です。
  • あくまで「スケジューリングのヒント」— React が実際に後回しにできるときのみ後回しにします。
💻 🅰️ 通常の setState — 入力がカクつく
// ❌ 譲歩なし — 入力ごとに5万件をフィルタリング
import { useState } from 'react';

const HUGE = Array.from({ length: 50000 }, (_, i) => `item-${i}`);

export function Search() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState<string[]>([]);

  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setQuery(e.target.value);
    const filtered = HUGE.filter(d => d.includes(e.target.value));
    setResults(filtered);
    // 高速でタイピングすると入力が1文字遅れる (jank)
  };

  return (
    <div>
      <input value={query} onChange={onChange} />
      <ul>{results.slice(0, 100).map(r => <li key={r}>{r}</li>)}</ul>
    </div>
  );
}
💻 🅱️ useTransition / useDeferredValue — 滑らかな入力
// ✅ useTransition — フィルタリングを譲歩
import { useState, useTransition, useDeferredValue } from 'react';

const HUGE = Array.from({ length: 50000 }, (_, i) => `item-${i}`);

export function Search() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState<string[]>(HUGE);
  const [isPending, startTransition] = useTransition();

  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setQuery(e.target.value); // 緊急

    startTransition(() => {
      // 非緊急 — 譲歩可能
      const filtered = HUGE.filter(d => d.includes(e.target.value));
      setResults(filtered);
    });
  };

  return (
    <div>
      <input value={query} onChange={onChange} />
      {isPending && <p>更新中...</p>}
      <ul>{results.slice(0, 100).map(r => <li key={r}>{r}</li>)}</ul>
    </div>
  );
}

// または useDeferredValue — 子が重い場合
export function SearchAlt() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <HeavyList query={deferredQuery} />
    </div>
  );
}

function HeavyList({ query }: { query: string }) {
  // query が遅れているため、入力には影響なし
  const filtered = HUGE.filter(d => d.includes(query));
  return <ul>{filtered.slice(0, 100).map(r => <li key={r}>{r}</li>)}</ul>;
}

💡 💡 useTransition / useDeferredValue 実践の5つのポイント

1. 入力は即時、結果は後回し — 最もよくあるパターン

tsx
setQuery(input);                  // 緊急
startTransition(() => setResults(filter(input))); // 後回し

2. isPending で進行状態を表示

tsx
{isPending && <Spinner />}

3. fetch のレスポンス処理は後回し可能、fetch 自体は後回し不可

tsx
const data = await fetch(url).then(r => r.json());
startTransition(() => setData(data));

4. useDeferredValue は「受け取った値」単位、useTransition は「自分が呼び出す」単位
親から受け取った props が頻繁に変わり、それを受け取る子が重い場合は useDeferredValue を使います。

5. デバウンスの部分的な代替であり、完全な代替ではない
useTransition はタイムスライシング、デバウンスは呼び出し自体を減らします。通常はどちらか一方を選び、状況に応じて使い分けます。

⚡ 実際に試してみる — startTransition のフロー

緊急な更新と後回し可能な更新の優先順位をシミュレーションします。
✏️ JS 코드
📟 コンソール出力
▶ 実行ボタンを押してください
⚠️ ブラウザのサンドボックスで実行 — console.log()のみ対応、alert/fetchは不可

確認クイズ

検索ボックスへの入力中、オートコンプリートのレンダリングが重くて入力がカクつきます。最も適切なツールは何ですか?
💡 useTransition(直接 setState を譲渡)または useDeferredValue(受け取った値を一拍遅らせる)が React 18+ の正解です。入力の更新は緊急として即座に反映され、重いオートコンプリートのレンダリングは割り込み可能としてマークされ、React がタイムスライシングでスムーズに処理します。useMemo はキャッシング用であり、setTimeout は古いデバウンスのパターンです。
useTransition / useDeferredValue - React