本文へスキップ
ウェブエンジニア問題集
useTransition

useTransition — 重い画面更新でも操作を止めない

約12分
この章の目次開く

検索欄へ文字を入力したとき、その下にある大きな一覧も同時に更新すると、入力が引っかかることがあります。 これは、入力欄の更新と重い一覧の再レンダーが、同じタイミングで処理されるためです。

useTransition を使うと、入力欄のようにすぐ反映したい更新と、検索結果のように少し遅れてもよい更新を分けられます。

学習者学習者

transition って、画面をふわっと切り替えるアニメーションのこと?

先生先生

ここでのTransitionはアニメーションではなく、更新の優先度をReactへ伝える仕組みだよ。急ぐ更新を先に表示し、重い更新はバックグラウンドで進められるんだ。


30秒解説 — useTransitionとは

useTransition は、state更新の一部を緊急ではない更新(Transition)として扱うためのフックです。

const [isPending, startTransition] = useTransition();
 
startTransition(() => {
  setSelectedTab(nextTab);
});
tsx
  • isPending:Transitionが処理中なら true
  • startTransition:内側で行うstate更新をTransitionとしてマークする関数

通常のstate更新は、クリックや入力にすぐ反映されます。一方、Transitionとしてマークした更新は、より緊急な更新が来れば中断して後からやり直せます。

useTransition は処理そのものを速くするのではなく、急ぐ更新を先に表示して、画面を操作できる状態に保つためのフックです。

構文・引数・戻り値

useTransition

構文:const [isPending, startTransition] = useTransition()

useTransition に引数はありません。

戻り値型説明
isPendingbooleanTransitionが処理中なら true、完了していれば false
startTransition(action) => voidaction 内のstate更新をTransitionとしてマークする関数

戻り値:isPending と startTransition の2要素を持つ配列

useState と同様に、分割代入して使うのが基本です。

startTransition

構文:startTransition(action)

引数渡せるもの説明
action引数を取らない関数関数の実行中に行われたstate更新をTransitionとしてマークする

戻り値:なし

React 19の公式ドキュメントでは、startTransition に渡す関数をアクション(Action)と呼びます。


緊急な更新とTransition更新

たとえば、検索画面には次の2種類の更新があります。

更新優先度理由
入力欄へ文字を表示する緊急キー入力にすぐ反応しないと、操作が壊れたように感じる
大きな検索結果一覧を更新するTransitionにできる数十ミリ秒遅れても入力操作は続けられる

useTransition を使うと、Reactは次のように処理できます。

Transition中に次のキー入力が来ると、Reactは一覧のレンダーを中断して、入力欄の更新を優先できます。その後、最新の検索条件で一覧をレンダーし直します。


基本例 — タブ切り替えをTransitionにする

タブを切り替えると重い内容が表示される画面を考えます。

import { useState, useTransition } from 'react';
 
type Tab = 'overview' | 'reviews' | 'related';
 
function ProductTabs() {
  const [tab, setTab] = useState<Tab>('overview');
  const [isPending, startTransition] = useTransition();
 
  function selectTab(nextTab: Tab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }
 
  return (
    <section>
      <nav>
        <button onClick={() => selectTab('overview')}>概要</button>
        <button onClick={() => selectTab('reviews')}>レビュー</button>
        <button onClick={() => selectTab('related')}>関連商品</button>
      </nav>
 
      {isPending && <p>切り替え中...</p>}
      <TabContent tab={tab} />
    </section>
  );
}
tsx

setTab(nextTab) を startTransition の中で呼ぶと、タブ内容の更新がTransitionになります。新しい内容のレンダー中も、ユーザーは別のタブをクリックできます。

isPending は、Transitionを開始してから新しい画面が表示されるまで true になります。これを使って「切り替え中」と表示したり、対象部分を少し薄くしたりできます。

処理が十分に速い場合は、isPending が true の画面を人が認識できないこともあります。これは異常ではありません。


実践例 — 入力欄を止めずに重い一覧を更新する

検索入力では、入力欄用のstateと検索結果用のstateを分けます。

import { memo, useState, useTransition } from 'react';
 
type Product = {
  id: number;
  name: string;
};
 
const products: Product[] = [
  { id: 1, name: 'キーボード' },
  { id: 2, name: 'マウス' },
  { id: 3, name: 'ディスプレイ' },
  // 実際には大量の商品がある想定
];
 
const ProductList = memo(function ProductList({ query }: { query: string }) {
  const normalizedQuery = query.toLowerCase();
  const visibleProducts = products.filter((product) =>
    product.name.toLowerCase().includes(normalizedQuery),
  );
 
  return (
    <ul>
      {visibleProducts.map((product) => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
});
 
export default function ProductSearch() {
  const [query, setQuery] = useState('');
  const [resultQuery, setResultQuery] = useState('');
  const [isPending, startTransition] = useTransition();
 
  function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
    const nextQuery = event.target.value;
 
    // 緊急な更新:入力欄へすぐ表示する
    setQuery(nextQuery);
 
    // 後回しにできる更新:重い一覧を更新する
    startTransition(() => {
      setResultQuery(nextQuery);
    });
  }
 
  return (
    <section>
      <label>
        商品を検索
        <input value={query} onChange={handleChange} />
      </label>
 
      <div aria-busy={isPending}>
        {isPending && <p>検索結果を更新中...</p>}
        <ProductList query={resultQuery} />
      </div>
    </section>
  );
}
tsx

更新の流れは次のとおりです。

  1. setQuery(nextQuery) が入力欄をすぐ更新する
  2. setResultQuery(nextQuery) はTransition更新として始まる
  3. ProductList が新しい検索語でバックグラウンドレンダーされる
  4. 途中で次の文字が入力されたら、Reactは古い一覧のレンダーを中断する
  5. 入力が落ち着いたら、最新の検索結果を表示する

ProductList を memo で包んでいるのは、入力欄だけを更新する緊急レンダーで、古い検索語の一覧をもう一度重くレンダーしないためです。memo と関数・値の参照については、前章のuseCallbackでも扱いました。

制御された入力欄のstateは通常どおり更新し、重い結果表示に使うstateだけをTransitionにします。

テキスト入力のstateをTransitionにしてはいけない

次の書き方は避けてください。

function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
  startTransition(() => {
    setQuery(event.target.value); // NG:入力欄を制御するstate
  });
}
tsx

制御された入力欄では、value に渡すstateをキー入力と同期して更新する必要があります。Transition更新は中断される可能性があるため、テキスト入力の制御には使えません。

入力値がそのまま重いコンポーネントへ渡されていて、更新用のstateを分けたくない場合は、後述する useDeferredValue が候補になります。


useMemoとの違い

useMemo と useTransition はどちらも、重い画面で登場することがあります。しかし、役割は異なります。

フック目的減らせるもの
useMemo計算結果を再利用する同じ依存値に対する計算のやり直し
useTransition更新の優先度を下げる緊急な更新が重いレンダーを待つ時間

useTransition は、1回分のレンダーや計算自体を軽くしません。重いレンダーを後回しにして中断可能にする仕組みです。逆に、useMemo は計算結果を再利用しますが、実行が必要になった計算を中断可能にはしません。

両方が必要な場合もありますが、まずReact DevToolsのProfilerなどで遅い場所を確認してから選びます。


useDeferredValueとの使い分け

useDeferredValue も、重い表示を遅らせて操作の応答性を保つフックです。

const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
tsx

判断基準は、どのstate更新を後回しにするか自分で指定できるかです。

状況選ぶもの
stateの更新関数を自分で呼べるuseTransition で更新を囲む
propsなど、受け取った値しか触れないuseDeferredValue で値の反映を遅らせる
処理中かどうかを表示したいisPending がある useTransition

useDeferredValue は値の遅延版を返しますが、isPending は返しません。元の値と遅延値を比較すれば、古い内容を表示中かどうかは判断できます。

どちらも「500ミリ秒待つ」といった固定時間の遅延や、入力回数をまとめるデバウンスではありません。Reactが端末の処理状況と更新の優先度に応じてレンダーを進めます。


非同期処理で使うときの注意

React 19では、startTransition に async 関数を渡し、非同期処理の完了まで isPending で追跡できます。

const [isPending, startTransition] = useTransition();
 
function handleSave(formData: FormData) {
  startTransition(async () => {
    const savedItem = await saveItem(formData);
 
    // await後のstate更新は、現在はもう一度囲む
    startTransition(() => {
      setItem(savedItem);
    });
  });
}
tsx

現在のReactには、await の後に行うstate更新をもう一度 startTransition で囲む必要があるという制限があります。

また、Transitionは通信を高速化したり、同じリクエストをまとめたり、古いリクエストを自動で中止したりする機能ではありません。通信のキャンセルや競合対策は別に設計する必要があります。


よくあるハマりどころ

startTransitionの中へ重い処理を書けば速くなると思う

startTransition に渡した関数はすぐ実行されます。そのため、イベントハンドラ内の重いJavaScript処理は、そのまま操作をブロックします。

startTransition(() => {
  const sortedItems = veryExpensiveSort(items); // この計算はすぐ実行される
  setItems(sortedItems);
});
tsx

useTransition が中断可能にするのはReactのレンダーです。イベントハンドラ内の重い計算には、処理方法の見直しやWeb Workerなど別の対策が必要です。

setTimeout内の更新もTransitionになると思う

次の setTab は、startTransition の実行が終わった後に呼ばれるため、Transition更新になりません。

startTransition(() => {
  setTimeout(() => {
    setTab('reviews'); // Transitionにならない
  }, 1000);
});
tsx

必要なら、実際にstateを更新する場所で囲みます。

setTimeout(() => {
  startTransition(() => {
    setTab('reviews');
  });
}, 1000);
tsx

CSSアニメーションの機能だと思う

useTransition は更新の優先度を扱うフックであり、CSSの transition プロパティとは別物です。名前が似ていても、フェードやスライドを自動で追加する機能ではありません。


ちゃんと使うためのポイント

  • まず通常のstate更新で実装し、本当に操作が重い場所で使う
  • 入力欄や押下状態など、即時反映が必要な更新はTransitionにしない
  • 一覧・グラフ・タブ内容など、少し遅れてもよい重い更新だけを囲む
  • isPending で、古い画面が表示されている理由をユーザーへ伝える
  • 処理そのものを速くする機能ではないため、不要な計算やレンダーも減らす
  • 固定時間のデバウンスや、非同期通信のキャンセルとは分けて考える

まとめ

useTransition は、state更新の一部を緊急ではないTransitionとしてマークするフックです。

startTransition の中でstateを更新すると、その更新によるレンダーは中断可能になります。isPending を使えば、処理中であることも画面に表示できます。

入力欄のような緊急な更新は通常どおり行い、重い一覧やタブ内容など、後回しにできる更新だけをTransitionにするのが基本です。

useMemo は計算を再利用する仕組み、useDeferredValue は値の反映を遅らせる仕組みです。目的を区別し、実際に操作が重い場所へ必要なものだけを使ってください。

参考リンク

Reactクイズに挑戦するReact Hooksの基礎と使い分けを4択クイズで復習しよう

次の章では、ここまで学んだ7つのフックを「どの場面で選ぶか」という視点で横断的に整理します。詳しくはHooksの使い分けへ進んでください。