Menu

React useTransitionの使い方:UIの応答性を保つ

useTransitionはstateの更新を緊急でないものとして扱うので、Reactは遅い部分を裏でレンダリングしながらページの応答性を保てます。isPending、startTransition、React 19の非同期トランジション、そしてデバウンスとは違う理由を学びます。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

useTransition を使うと、stateの更新を緊急でないものとして扱えます。Reactはその更新を裏でレンダリングし、クリックやキー入力のようにもっと緊急なことが起きたら、それを捨てるかやり直します。そのため、画面の遅い部分が追いつく間も、ページの応答性が保たれます。

postsをクリックし、リストが表示される前にcontactをクリックしてください。contactはすぐに表示されます。Reactが途中までのpostsのレンダリングを捨てたのです。次に startTransition(() => { setTab(next); }) を普通の setTab(next); に変えて同じことを試してください。postsをクリックした後、ページが約300ミリ秒固まり、contactのクリックはpostsが終わるまで待たされます。

構文

const [isPending, startTransition] = useTransition();

startTransition(() => {
    setTab(next);
});
  • startTransition は関数を受け取ってすぐに実行します。その中のstateの更新は、すべてトランジションとして扱われます。
  • isPending は、トランジションを始めた瞬間から、Reactがそのレンダリングを終えて結果をコミットするまで true です。

useTransition は引数を取りません。渡した関数は同期的に実行されます。トランジションが関わるのは、結果として起きる更新をReactがどうレンダリングするかであって、関数がいつ実行されるかではありません。

「緊急でない」の意味

Reactは更新を2種類に分けます。入力、クリック、押下のような緊急の更新は、すぐに反映されないとページが壊れているように感じられます。新しい画面への切り替えやフィルター結果の表示のようなトランジションの更新は、少し時間がかかっても構いません。

Reactはトランジションを小さく区切ってレンダリングし、コンポーネントの合間に緊急のものが来ていないかを確認します。来ていれば、トランジションを一時停止するか放棄し、緊急の更新を処理してから、最新のstateでトランジションをやり直します。トランジションのレンダリング中は前の画面が表示されたままで、Reactは組み立て途中の結果を表示しません。例でpostsの準備ができるまで、ボタンの横にLoadingの表示を出しつつ、aboutのテキストが表示され続けるのはそのためです。

中断はコンポーネントの合間で起きます。300ミリ秒止まる1つのコンポーネントを半分に切ることはできません。上の例で遅い処理を300個の小さな SlowPost コンポーネントに分けているのはそのためです。

保留中の表示を出す

isPending は、現在の中身を置き換えない読み込み中の表示に使います。ボタンの横のスピナー、薄くしたテキスト、「Saving...」のラベルなどです。最初の例では、タブの横に Loading... を表示しています。中身のスタイルに使うこともできます。

<div style={{ opacity: isPending ? 0.6 : 1 }}>{content}</div>

入力中に大きなリストをフィルターする

入力欄そのものの値をトランジションに入れることはできません。制御された <input> は同じ緊急の更新で新しい値を受け取らないと、文字が抜け落ちます。代わりに2つのstateを持ちます。入力欄のテキスト(緊急)と、リストが使う検索語(トランジション)です。

123 とすばやく入力してください。入力欄はまったく遅れません。Results はReactがレンダリングを始めた時点でログを出すので、コンソールにはキーを押すたびに rendering results が1行ずつ記録されますが、1 と 12 のリストが画面に届くことはありません。新しいキー入力が進行中のレンダリングを中断し、Reactが最新の検索語でやり直したからです。Results をあえて memo で包んでいます。これがないと、キー入力ごとの緊急のレンダリングでも、古い検索語で遅いリストがレンダリングされ、効果が失われてしまいます。

フックなしのstartTransition

react は startTransition を普通の関数としてもエクスポートしています。フックと同じように更新をトランジションとして扱いますが、isPending は得られません。データライブラリや、コンポーネントの外にあるルーターのように、フックを呼べない場所で使います。

import { startTransition } from 'react';

function navigate(url) {
    startTransition(() => {
        setCurrentUrl(url);
    });
}

React 19の非同期トランジション

React 19からは、渡す関数を async にできます。そうすると isPending は待ち時間も含めて関数の実行中ずっと true のままなので、リクエスト中に保留中の状態を表示する簡単な方法になります。下の偽の saveName は本物のリクエストの代わりです。

setSaved を囲む2つ目の startTransition に注目してください。Reactは await をまたぐとトランジションを追跡できなくなるので、その後のstateの更新をトランジションの一部にするには、もう一度包む必要があります。このように渡された関数を、Reactはアクションと呼びます。useActionStateやフォームアクションは同じ仕組みの上に作られています。

トランジションはデバウンスではない

デバウンスは、最後のキー入力から決まった時間待ってから何かをします。トランジションは待ちません。すぐにレンダリングを始め、新しい更新が来れば古い作業を捨てます。速い端末では遅延がまったくなく結果が表示され、遅い端末ではReactがキー入力の合間にできるだけのことをします。トランジションにできないのは、ネットワークリクエストの数を減らすことです。キー入力のたびにfetchを始めるなら、fetchをデバウンスしてください。

トランジションとSuspense

トランジションは Suspense 境界の振る舞いも変えます。普通、境界の中のコンポーネントがサスペンドすると(lazyコンポーネントの読み込みや、use で読むデータ)、Reactは中身をフォールバックに置き換えます。サスペンドを起こした更新がトランジションなら、Reactは代わりにすでに画面にある中身を保ち、新しい中身の準備ができるまで isPending は true のままです。ルーターがナビゲーションのたびにページ全体のスピナーをちらつかせずに済むのはこのためです。ナビゲーションをトランジションで包むので、次のページを描画できるまで古いページが表示されたままになります。一度も表示されていない中身はやはりフォールバックを使うので、新しく現れる境界はいつもどおりに振る舞います。

うまくいかないこと

  • タイムアウトの中の更新。startTransition(() => setTimeout(() => setTab(next), 100)) は更新をトランジションとして扱いません。startTransition が戻った後に実行されるからです。代わりにタイムアウトの中に startTransition を入れてください。
  • 1つの遅いコンポーネント。Reactが中断できるのはコンポーネントの合間だけなので、長い時間止まる1つのコンポーネントはやはり止まります。
  • 入力欄の制御。上で述べたとおり、入力欄そのもののstateは緊急のままです。

stateのセッターを自分で持っておらず、propのように値を受け取るだけの場合は、useDeferredValueが同じ振る舞いをしてくれます。

よくある質問

useTransitionは何をするものですか?

中のstateの更新を緊急でないものとして扱う startTransition と、その更新のレンダリング中に true になる isPending を返します。Reactは緊急でないレンダリングを中断して、クリックや入力を先に処理できます。

useTransitionとstartTransitionの違いは何ですか?

どちらも更新をトランジションとして扱います。useTransition フックは、読み込み中の表示のための isPending も返します。react からインポートする単独の startTransition はコンポーネントの外でも動きますが、保留中フラグはありません。

テキスト入力にuseTransitionを使えますか?

入力欄そのものの値には使えません。制御された入力は同期的に更新しなければ、入力が壊れます。入力欄のstateは緊急のままにし、それに依存する重い更新をトランジションに入れるか、useDeferredValue を使ってください。

useTransitionはデバウンスと同じですか?

いいえ。デバウンスは何かをする前に決まった時間待ちます。トランジションはすぐにレンダリングを始め、新しい更新が来れば中断されるので、速い端末では遅延なしに結果が表示されます。

startTransitionにasync関数を渡せますか?

はい、React 19から渡せます。isPending はasync関数が終わるまで true のままです。await の後のstateの更新をトランジションとして扱うには、もう一度 startTransition で包む必要があります。

Coddyのプログラミング言語のイラスト

Coddyでコードを学ぼう

始める