useTransition は、UI を部分的にバックグラウンドでレンダーするための React フックです。

const [isPending, startTransition] = useTransition()

リファレンス

useTransition()

コンポーネントのトップレベルで useTransition を呼び出し、state 更新の一部をトランジションとしてマークします。

import { useTransition } from 'react';

function TabContainer() {
const [isPending, startTransition] = useTransition();
// ...
}

さらに例を見る

引数

useTransition には引数はありません。

返り値

useTransition は常に 2 つの要素を含む配列を返します。

  1. トランジションが保留中であるかどうかを示す isPending フラグ。
  2. 更新をトランジションとしてマークするための startTransition 関数

startTransition(action)

useTransition によって返される startTransition 関数により、更新をトランジションとしてマークすることができます。

function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');

function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ...
}

補足

Functions called in startTransition are called “Actions”.

The function passed to startTransition is called an “Action”. By convention, any callback called inside startTransition (such as a callback prop) should be named action or include the “Action” suffix:

function SubmitButton({ submitAction }) {
const [isPending, startTransition] = useTransition();

return (
<button
disabled={isPending}
onClick={() => {
startTransition(() => {
submitAction();
});
}}
>
Submit
</button>
);
}

引数

  • action: 1 つ以上の set 関数を呼び出して state を更新する関数。React は引数なしで直ちに action を呼び出し、action 関数呼び出し中に同期的にスケジュールされたすべての state 更新をトランジションとしてマークします。action 内で await されている非同期関数のコールもトランジションの一部ではありますが、現時点では await の後に来る set 関数は別の startTransition にラップする必要があります(トラブルシューティング参照)。トランジションとしてマークされた state の更新はノンブロッキングになり、不要なローディングインジケータを表示しないようになります。

返り値

startTransition は何も返しません。

注意点

  • useTransition はフックであるため、コンポーネント内かカスタムフック内でのみ呼び出すことができます。他の場所(例えば、データライブラリ)でトランジションを開始する必要がある場合は、代わりにスタンドアロンの startTransition を呼び出してください。

  • state の set 関数にアクセスできる場合にのみ、state 更新をトランジションにラップできます。ある props やカスタムフックの値に反応してトランジションを開始したい場合は、代わりに useDeferredValue を試してみてください。

  • startTransition に渡された関数は即座に呼び出され、その関数の実行中に発生するすべての state 更新がトランジションとしてマークされます。しかし例えば、setTimeout 内で state を更新しようとした場合は、それはトランジションとしてマークされません。

  • 非同期リクエスト後に state 更新を行いたい場合は、トランジションとしてマークするために別の startTransition でラップする必要があります。これは既知の制限であり、将来的に修正される予定です(詳細はトラブルシューティングを参照してください)。

  • startTransition 関数は常に同一のものとなるため、多くの場合エフェクトの依存配列では省略されますが、依存配列に含めてもエフェクトの再実行は起こりません。依存値を削除してもリンタがエラーを出さない場合、削除しても安全です。エフェクトから依存値を取り除く方法を参照してください。

  • トランジションとしてマークされた state 更新は、他の state 更新によって中断されます。例えば、トランジション内でチャートコンポーネントを更新した後、チャートの再レンダーの途中で入力フィールドに入力を始めた場合、React は入力欄の更新の処理後にチャートコンポーネントのレンダー作業を再開します。

  • トランジションによる更新はテキスト入力欄の制御には使用できません。

  • 進行中のトランジションが複数ある場合、React は現在それらをひとつに束ねる処理を行います。この制限は将来のリリースでは削除される可能性があります。

使用法

ノンブロッキングな更新をアクションを使って実行する

コンポーネントのトップレベルで useTransition を呼び出してアクション (Action) を作成し、保留中 (pending) 状態にアクセスします。

import {useState, useTransition} from 'react';

function CheckoutForm() {
const [isPending, startTransition] = useTransition();
// ...
}

useTransition は正確に 2 つの項目を含む配列を返します:

  1. トランジションが保留中であるかどうかを示す isPending フラグ
  2. アクションを作成するための startTransition 関数

トランジションを開始するには、以下のようにして startTransition に関数を渡します。

import {useState, useTransition} from 'react';
import {updateQuantity} from './api';

function CheckoutForm() {
const [isPending, startTransition] = useTransition();
const [quantity, setQuantity] = useState(1);

function onSubmit(newQuantity) {
startTransition(async function () {
const savedQuantity = await updateQuantity(newQuantity);
startTransition(() => {
setQuantity(savedQuantity);
});
});
}
// ...
}

startTransition に渡される関数が “アクション (Action)” と呼ばれるものです。アクション内では state を更新したり、(必要に応じて)副作用を実行したりすることができます。その作業はバックグラウンドで、ページ上のユーザ操作をブロックすることなく行われます。ひとつのトランジションが複数のアクションを含むことができ、トランジションが進行中でも UI の応答性は保たれます。例えば、ユーザがタブをクリックしたあとに気が変わって別のタブをクリックした場合でも、最初の更新が終了するのを待つことなく、2 回目のクリックが即座に処理されます。

トランジションの進行中状態に関するフィードバックをユーザに提供するために、startTransition が最初に呼び出されると isPending state が true に切り替わり、すべてのアクションが完了して最終的な状態がユーザに表示されるまで true のままになります。トランジションによりアクション内の副作用が順番に完了することが保証され、不要なローディングインジケータが抑止されます。また、useOptimistic を使用することで、トランジションが進行中の間にも即時のフィードバックを提供することができます。

useTransition と通常の state 更新の違い

1/2:
アクションで数量を更新

この例では、updateQuantity 関数がカート内の商品の数量を更新するリクエストをサーバに送信する部分をシミュレーションしています。この関数は、リクエストの完了に少なくとも 1 秒かかるように意図的に遅延させられています。

数量欄を素早く複数回更新してみてください。リクエストが進行中の間、“Total” 欄には保留中状態が表示され、最後のリクエストが完了した後にのみ “Total” が更新されることに注意してください。更新がアクション内で行われるため、リクエストの進行中でも “quantity” 欄を更新し続けることが可能です。

import { useState, useTransition } from "react";
import { updateQuantity } from "./api";
import Item from "./Item";
import Total from "./Total";

export default function App({}) {
  const [quantity, setQuantity] = useState(1);
  const [isPending, startTransition] = useTransition();

  const updateQuantityAction = async newQuantity => {
    // To access the pending state of a transition,
    // call startTransition again.
    startTransition(async () => {
      const savedQuantity = await updateQuantity(newQuantity);
      startTransition(() => {
        setQuantity(savedQuantity);
      });
    });
  };

  return (
    <div>
      <h1>Checkout</h1>
      <Item action={updateQuantityAction}/>
      <hr />
      <Total quantity={quantity} isPending={isPending} />
    </div>
  );
}

これはアクションの動作を示す基本的な例となっていますが、この例ではリクエストが順番通り完了しなかった場合の問題を処理していません。数量を複数回更新すると、後続のリクエストの後で以前のリクエストが完了するために、数量がおかしな順番で更新されてしまう可能性があります。これは既知の制限であり、将来的に修正される予定です(詳細はトラブルシューティングを参照してください)。

React は、一般的なユースケースに対応する以下のような組み込みの抽象化を提供しています。

これらのソリューションはリクエスト順序の問題を自動的に管理します。トランジションを使って非同期の state 遷移を管理するカスタムフックやライブラリを構築する場合、リクエスト順序をより高度に制御可能ですが、問題を手動で管理する必要があります。


Exposing action prop from components

コンポーネントが props として action を公開することで、親がアクションを呼び出せるようにできます。

例えばこの TabButton コンポーネントは onClick 時のロジックを action 内にラップしています。

export default function TabButton({ action, children, isActive }) {
const [isPending, startTransition] = useTransition();
if (isActive) {
return <b>{children}</b>
}
return (
<button onClick={() => {
startTransition(() => {
action();
});
}}>
{children}
</button>
);
}

これで親コンポーネントは state を action 内で更新するようになるため、この state 更新はトランジションとしてマークされます。つまり、“Posts” をクリックした直後に “Contact” をクリックしても、ユーザ操作がブロックされないようになるということです。

import { useTransition } from 'react';

export default function TabButton({ action, children, isActive }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        action();
      });
    }}>
      {children}
    </button>
  );
}


保留中状態を視覚的に表示する

useTransition によって返される isPending ブーリアン値を使用して、ユーザにトランジションが進行中であることを示すことができます。例えば、タブボタンは特別な “pending” という視覚状態を持つことができます。

function TabButton({ action, children, isActive }) {
const [isPending, startTransition] = useTransition();
// ...
if (isPending) {
return <b className="pending">{children}</b>;
}
// ...

“Posts” をクリックすると、タブボタン自体がすぐに更新されるため、より反応が良く感じられることに着目してください。

import { useTransition } from 'react';

export default function TabButton({ action, children, isActive }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  if (isPending) {
    return <b className="pending">{children}</b>;
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        action();
      });
    }}>
      {children}
    </button>
  );
}


望ましくないローディングインジケータの防止

この例では、PostsTab コンポーネントは use を使用していくつかのデータをフェッチしています。“Posts” タブをクリックすると、PostsTab コンポーネントがサスペンドし、その結果、最も近いローディングフォールバックが表示されます:

import { Suspense, useState } from 'react';
import TabButton from './TabButton.js';
import AboutTab from './AboutTab.js';
import PostsTab from './PostsTab.js';
import ContactTab from './ContactTab.js';

export default function TabContainer() {
  const [tab, setTab] = useState('about');
  return (
    <Suspense fallback={<h1>🌀 Loading...</h1>}>
      <TabButton
        isActive={tab === 'about'}
        action={() => setTab('about')}
      >
        About
      </TabButton>
      <TabButton
        isActive={tab === 'posts'}
        action={() => setTab('posts')}
      >
        Posts
      </TabButton>
      <TabButton
        isActive={tab === 'contact'}
        action={() => setTab('contact')}
      >
        Contact
      </TabButton>
      <hr />
      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </Suspense>
  );
}

ローディングインジケータを表示するためにタブのコンテナ全体が隠れることは不快なユーザ体験となってしまいます。TabButtonuseTransition を追加すると、代わりにタブボタン内に保留状態を表示することができます。

“Posts” をクリックしても、もはやタブコンテナ全体がスピナに置き換わることはなくなったことに注目してください。

import { useTransition } from 'react';

export default function TabButton({ action, children, isActive }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  if (isPending) {
    return <b className="pending">{children}</b>;
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        action();
      });
    }}>
      {children}
    </button>
  );
}

サスペンスとトランジションの詳細はこちらをご覧ください

補足

トランジションは(今回のタブコンテナのような)すでに表示されているコンテンツを隠さない範囲で「待機」を行います。もし Posts タブにネストした <Suspense> バウンダリがある場合、トランジションはそれを「待機」することはありません。


サスペンス対応ルータの構築

React のフレームワークやルータを構築している場合、ページのナビゲーションをトランジションとしてマークすることをお勧めします。

function Router() {
const [page, setPage] = useState('/');
const [isPending, startTransition] = useTransition();

function navigate(url) {
startTransition(() => {
setPage(url);
});
}
// ...

これが推奨されるのは以下の 3 つの理由からです:

以下は、ナビゲーションにトランジションを使用した簡易的なルータの例です。

import { Suspense, useState, useTransition } from 'react';
import IndexPage from './IndexPage.js';
import ArtistPage from './ArtistPage.js';
import Layout from './Layout.js';

export default function App() {
  return (
    <Suspense fallback={<BigSpinner />}>
      <Router />
    </Suspense>
  );
}

function Router() {
  const [page, setPage] = useState('/');
  const [isPending, startTransition] = useTransition();

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

  let content;
  if (page === '/') {
    content = (
      <IndexPage navigate={navigate} />
    );
  } else if (page === '/the-beatles') {
    content = (
      <ArtistPage
        artist={{
          id: 'the-beatles',
          name: 'The Beatles',
        }}
      />
    );
  }
  return (
    <Layout isPending={isPending}>
      {content}
    </Layout>
  );
}

function BigSpinner() {
  return <h2>🌀 Loading...</h2>;
}

補足

サスペンス対応のルータは、デフォルトでナビゲーションの更新をトランジションにラップすることが期待されます。


エラーバウンダリでユーザにエラーを表示する

startTransition に渡された関数がエラーをスローした場合、エラーバウンダリを使用してユーザにエラーを表示することができます。エラーバウンダリを使用するには、useTransition を呼び出しているコンポーネントをエラーバウンダリで囲みます。startTransition に渡された関数がエラーになった場合、エラーバウンダリに指定されているフォールバックが表示されます。

import { useTransition } from "react";
import { ErrorBoundary } from "react-error-boundary";

export function AddCommentContainer() {
  return (
    <ErrorBoundary fallback={<p>⚠️Something went wrong</p>}>
      <AddCommentButton />
    </ErrorBoundary>
  );
}

function addComment(comment) {
  // For demonstration purposes to show Error Boundary
  if (comment == null) {
    throw new Error("Example Error: An error thrown to trigger error boundary");
  }
}

function AddCommentButton() {
  const [pending, startTransition] = useTransition();

  return (
    <button
      disabled={pending}
      onClick={() => {
        startTransition(() => {
          // Intentionally not passing a comment
          // so error gets thrown
          addComment();
        });
      }}
    >
      Add comment
    </button>
  );
}


トラブルシューティング

トランジション中に入力フィールドを更新できない

入力フィールドを制御する state 変数に対してトランジションを使用することはできません。

const [text, setText] = useState('');
// ...
function handleChange(e) {
// ❌ Can't use Transitions for controlled input state
startTransition(() => {
setText(e.target.value);
});
}
// ...
return <input value={text} onChange={handleChange} />;

これは、トランジションが非ブロッキングである一方、change イベントへの応答として入力を更新する処理は同期的である必要があるためです。タイピングに応じてトランジションを実行したい場合、2 つの選択肢があります:

  1. 入力フィールド用の state(常に同期的に更新される)と、トランジションで更新する state を別々に宣言する。これにより、同期的な state を使用して入力フィールドを制御しつつ、トランジション state 変数(入力欄より「遅れる」ことになる)をレンダーロジックの残りの部分に渡すことができます。
  2. あるいは、保持する state 変数は 1 つにし、実際の値より「遅れる」ことのできる useDeferredValue を追加することができます。これにより、ノンブロッキングな再レンダーを始めて、それが自動的に新しい値に「追いつく」ようにできます。

React が state 更新をトランジションとして扱わない

state 更新をトランジションでラップするとき、更新が startTransition の呼び出しの最中に行われていることを確認してください:

startTransition(() => {
// ✅ Setting state *during* startTransition call
setPage('/about');
});

startTransition に渡す関数は同期的でなければなりません。以下のような形で更新をトランジションとしてマークすることはできません。

startTransition(() => {
// ❌ Setting state *after* startTransition call
setTimeout(() => {
setPage('/about');
}, 1000);
});

代わりに、以下は可能です。

setTimeout(() => {
startTransition(() => {
// ✅ Setting state *during* startTransition call
setPage('/about');
});
}, 1000);

await 後の state 更新がトランジションにならない

startTransition 関数内で await を使用した場合、await の後に行われる state 更新はトランジションとしてマークされません。各 await 後の state 更新をそれぞれ startTransition 呼び出しでラップする必要があります。

startTransition(async () => {
await someAsyncFunction();
// ❌ Not using startTransition after await
setPage('/about');
});

一方で、以下は動作します。

startTransition(async () => {
await someAsyncFunction();
// ✅ Using startTransition *after* await
startTransition(() => {
setPage('/about');
});
});

これは JavaScript の制限により React が非同期コンテクストのスコープを失うために発生する問題です。将来的に AsyncContext が利用可能になれば、この制限は解消される予定です。


コンポーネントの外部から useTransition を呼び出したい

useTransition はフックであるため、コンポーネント外で呼び出すことはできません。この場合、代わりにスタンドアロンの startTransition メソッドを使用してください。同じように機能しますが、isPending インジケータは提供されません。


startTransition に渡す関数がすぐに実行される

このコードを実行すると、1、2、3 が出力されます:

console.log(1);
startTransition(() => {
console.log(2);
setPage('/about');
});
console.log(3);

1、2、3 が出力されるのは期待通りの動作ですstartTransition に渡す関数は遅延されません。ブラウザの setTimeout を使う場合とは異なり、コールバックは後で実行されるのではありません。React はあなたの関数をすぐに実行しますが、それが実行されている間にスケジュールされた state 更新をトランジションとしてマークします。以下のように動作していると考えることができます。

// A simplified version of how React works

let isInsideTransition = false;

function startTransition(scope) {
isInsideTransition = true;
scope();
isInsideTransition = false;
}

function setState() {
if (isInsideTransition) {
// ... schedule a Transition state update ...
} else {
// ... schedule an urgent state update ...
}
}

トランジション内で state 更新の順番がおかしくなる

startTransition 内で await を使用すると、更新が順不同で発生する可能性があります。

以下の例では、updateQuantity 関数がカート内の商品の数量を更新するリクエストをサーバに送信する部分をシミュレーションしています。この関数は、ネットワークリクエストの競合状態をシミュレートするため、初回リクエストの結果が常に後続リクエストの結果より後に返ってくるようになっています。

数量を一度だけ更新した場合と、素早く複数回更新した場合を試してみてください。誤った合計が表示される場合があることに気付くでしょう。

import { useState, useTransition } from "react";
import { updateQuantity } from "./api";
import Item from "./Item";
import Total from "./Total";

export default function App({}) {
  const [quantity, setQuantity] = useState(1);
  const [isPending, startTransition] = useTransition();
  // Store the actual quantity in separate state to show the mismatch.
  const [clientQuantity, setClientQuantity] = useState(1);
  
  const updateQuantityAction = newQuantity => {
    setClientQuantity(newQuantity);

    // Access the pending state of the transition,
    // by wrapping in startTransition again.
    startTransition(async () => {
      const savedQuantity = await updateQuantity(newQuantity);
      startTransition(() => {
        setQuantity(savedQuantity);
      });
    });
  };

  return (
    <div>
      <h1>Checkout</h1>
      <Item action={updateQuantityAction}/>
      <hr />
      <Total clientQuantity={clientQuantity} savedQuantity={quantity} isPending={isPending} />
    </div>
  );
}

複数回のクリックがあると、後続のリクエストが完了した後で古いリクエストが完了する場合があります。この場合、現在の React は意図した順序を認識できません。これは、更新が非同期的にスケジュールされ、非同期の境界を越えると React が順序の情報を保持できないからです。

トランジション内のアクションは実行順序を保証しないため、これは想定された動作です。一般的なユースケースのために、React は useActionState<form> アクション のような高レベルの抽象化を提供しており、順序の管理を自動化します。高度なユースケースでは、独自のキューイングや中断ロジックを実装して、実行順序を管理する必要があります。