第11回

useTransitionでUI応答性を向上させる:React 18コンカレント機能

React 18のuseTransitionとuseDeferredValueを解説します。重い状態更新を「緊急でない」としてマークし、入力やクリックへの応答を維持しながら重い処理を裏で実行する仕組みを学べます。

·11分で読める
たける
たける リストが5000件あって、フィルター入力のたびに一瞬UIが固まります。`useMemo` を試しても改善しなくて。
りこ
りこ `useMemo` は再計算を減らす最適化。5000件のレンダリング自体が重いなら `useTransition` で「この更新は急ぎじゃない」とReactに伝える。入力への応答だけ先に処理して、リストの更新を後回しにできる。

コンカレントレンダリングとは

React 18で導入されたコンカレント(並行)レンダリングは、レンダリング作業を中断・再開できる仕組みです。

従来のReactはレンダリングを一気に完了させるため、重い処理中はUIがフリーズしました。コンカレントレンダリングでは、優先度の高い更新(ユーザーの入力など)を優先しながら、優先度の低い更新(重いリスト描画など)を裏で処理できます。

useTransitionの基本

const [isPending, startTransition] = useTransition();
  • startTransition で囲んだ状態更新を「緊急でない(non-urgent)」とマークする
  • isPending — トランジション中は true
function SearchPage() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState<string[]>([]);
  const [isPending, startTransition] = useTransition();

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setQuery(e.target.value);  // 緊急:入力はすぐ反映

    // 緊急でない:重いリスト更新は後回しでよい
    startTransition(() => {
      setResults(filterItems(e.target.value));
    });
  };

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

startTransition なしだと、入力のたびに大量のリスト描画が走り、入力がもたつきます。startTransition を使うと入力フィールドは即座に反映され、リストは順次更新されます。

実践:タブ切り替えの応答性改善

重いコンテンツを持つタブ切り替えに使うと効果的です。

type TabId = 'articles' | 'comments' | 'bookmarks';

function ProfilePage({ userId }: { userId: number }) {
  const [activeTab, setActiveTab] = useState<TabId>('articles');
  const [isPending, startTransition] = useTransition();

  const switchTab = (tab: TabId) => {
    startTransition(() => {
      setActiveTab(tab);  // タブの切り替え自体は緊急でない
    });
  };

  return (
    <div>
      <div className="flex gap-4">
        {(['articles', 'comments', 'bookmarks'] as TabId[]).map((tab) => (
          <button
            key={tab}
            onClick={() => switchTab(tab)}
            style={{
              opacity: isPending ? 0.7 : 1,
              fontWeight: activeTab === tab ? 'bold' : 'normal',
            }}
          >
            {tab}
          </button>
        ))}
      </div>
      {/* isPendingの間は古いタブのコンテンツを表示し続ける */}
      {activeTab === 'articles' && <ArticleList userId={userId} />}
      {activeTab === 'comments' && <CommentList userId={userId} />}
      {activeTab === 'bookmarks' && <BookmarkList userId={userId} />}
    </div>
  );
}

トランジション中は古いUIを表示したまま新しいコンテンツを裏で準備します。ユーザーはローディングスピナーではなく、現在のコンテンツを見続けられます。

useDeferredValue:値を遅延させる

useDeferredValue は値のレンダリングを遅らせます。useTransition との違いは、state更新の発生元ではなく、受け取った値を遅延させる点です。

function ProductSearch({ products }: { products: Product[] }) {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);  // query の遅延版

  // deferredQuery が変わったときだけ再計算
  const filteredProducts = useMemo(() =>
    products.filter((p) =>
      p.name.toLowerCase().includes(deferredQuery.toLowerCase())
    ),
    [products, deferredQuery]
  );

  const isStale = query !== deferredQuery;  // 古い結果を表示中

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="商品を検索..."
      />
      <div style={{ opacity: isStale ? 0.7 : 1, transition: 'opacity 0.2s' }}>
        {filteredProducts.map((p) => (
          <div key={p.id}>{p.name}</div>
        ))}
      </div>
    </div>
  );
}

useTransitionとuseDeferredValueの使い分け

場面 推奨
自分でstateを更新できる useTransition
外部からpropsで受け取った値を遅延させたい useDeferredValue
Suspenseと組み合わせてデータ取得を遅延させる useDeferredValue

useTransitionを使わなくてよいケース

// ❌ 軽い更新に使う必要はない
const [isPending, startTransition] = useTransition();
startTransition(() => {
  setCount(count + 1);  // 単純なカウンターは緊急でないとする必要なし
});

// ✅ シンプルに
setCount(count + 1);

コンカレント機能は明らかに遅いUIを改善するための最終手段です。まず useMemouseCallback などの通常の最適化を試みてください。

たける
たける `useDeferredValue` とどう使い分けるんですか? 似たようなことができる気がして。
りこ
りこ 自分でstateを持っているなら `useTransition`。propsや外部から受け取った値を遅延させたいなら `useDeferredValue`。制御できる場所が違う。

まとめ

  • useTransition で重い状態更新を「緊急でない」とマークし、入力応答性を維持する
  • isPending でトランジション中であることをUIに反映できる
  • useDeferredValue は外部から受け取った値を遅延させる(propsの場合に使う)
  • 軽いUIには不要。まず通常の最適化を試みる

次の第12回では、Hooksを正しく使うためのルールとよくある間違いを学びます。