useTransition は、UI をブロックせずに state を更新するための React フックです。

const [isPending, startTransition] = useTransition()

リファレンス

useTransition()

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

import { useTransition } from 'react';

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

さらに例を見る

引数

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

返り値

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

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

startTransition 関数

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

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

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

引数

返り値

startTransition は何も返しません。

注意点

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

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

  • startTransition に渡す関数は同期的でなければなりません。React はこの関数を直ちに実行し、その実行中に行われるすべての state 更新をトランジションとしてマークします。後になって(例えばタイムアウト内で)さらに state 更新をしようとすると、それらはトランジションとしてマークされません。

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

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

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


使用法

state 更新をノンブロッキングのトランジションとしてマークする

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

import { useState, useTransition } from 'react';

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

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

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

その後、次のようにして state 更新をトランジションとしてマークできます。

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

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

トランジションを使用することで、遅いデバイスでもユーザインターフェースの更新をレスポンシブに保つことができます。

トランジションを使用すると、再レンダーの途中でも UI がレスポンシブに保たれます。例えば、ユーザがタブをクリックしたが、その後気が変わって別のタブをクリックする場合、最初の再レンダーが終了するのを待つことなくそれを行うことができます。

useTransition と通常の state 更新の違い

例 1/2:
トランジションで現在のタブを更新する

この例では、“Posts” タブが人為的に遅延させられているため、レンダーには少なくとも 1 秒かかります。

“Posts” をクリックし、すぐに “Contact” をクリックしてみてください。これにより、“Posts” の遅いレンダーが中断されます。“Contact” タブはすぐに表示されます。この state 更新はトランジションとしてマークされているため、遅い再レンダーがユーザインターフェースをフリーズさせることはありません。

import { useState, useTransition } 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 [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState('about');

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

  return (
    <>
      <TabButton
        isActive={tab === 'about'}
        onClick={() => selectTab('about')}
      >
        About
      </TabButton>
      <TabButton
        isActive={tab === 'posts'}
        onClick={() => selectTab('posts')}
      >
        Posts (slow)
      </TabButton>
      <TabButton
        isActive={tab === 'contact'}
        onClick={() => selectTab('contact')}
      >
        Contact
      </TabButton>
      <hr />
      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </>
  );
}


トランジション中に親コンポーネントを更新する

useTransition の呼び出しから親コンポーネントの state を更新することもできます。例えば、この TabButton コンポーネントは onClick のロジックをトランジションでラップしています。

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

親コンポーネントは onClick イベントハンドラ内で state を更新しているため、その state 更新はトランジションとしてマークされます。このため、前の例と同様に、“Post” をクリックした直後に “Contact” をクリックできます。選択されたタブの更新はトランジションとしてマークされているため、ユーザ操作をブロックしません。

import { useTransition } from 'react';

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


トランジション中に保留中状態を視覚的に表示する

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

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

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

import { useTransition } from 'react';

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


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

この例では、PostsTab コンポーネントはサスペンス (Suspense) 対応のデータソースを使用していくつかのデータをフェッチしています。“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'}
        onClick={() => setTab('about')}
      >
        About
      </TabButton>
      <TabButton
        isActive={tab === 'posts'}
        onClick={() => setTab('posts')}
      >
        Posts
      </TabButton>
      <TabButton
        isActive={tab === 'contact'}
        onClick={() => setTab('contact')}
      >
        Contact
      </TabButton>
      <hr />
      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </Suspense>
  );
}

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

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

import { useTransition } from 'react';

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

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

補足

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


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

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

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

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

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

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

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>;
}

補足

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


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

Canary

useTransition でのエラーバウンダリの使用は React の Canary と experimental バージョンでのみ使用可能です。React のリリースチャンネルについてはこちら。

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 Error('Example error')
  }
}

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);

同様に、以下のように更新をトランジションとしてマークすることはできません。

startTransition(async () => {
await someAsyncFunction();
// ❌ Setting state *after* startTransition call
setPage('/about');
});

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

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

コンポーネントの外部から 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 ...
}
}