useTransition
useTransition
は、UI を部分的にバックグラウンドでレンダーするための React フックです。
const [isPending, startTransition] = useTransition()
リファレンス
useTransition()
コンポーネントのトップレベルで useTransition
を呼び出し、state 更新の一部をトランジションとしてマークします。
import { useTransition } from 'react';
function TabContainer() {
const [isPending, startTransition] = useTransition();
// ...
}
引数
useTransition
には引数はありません。
返り値
useTransition
は常に 2 つの要素を含む配列を返します。
- トランジションが保留中であるかどうかを示す
isPending
フラグ。 - 更新をトランジションとしてマークするための
startTransition
関数。
startTransition(action)
useTransition
によって返される startTransition
関数により、更新をトランジションとしてマークすることができます。
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ...
}
引数
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 つの項目を含む配列を返します:
- トランジションが保留中であるかどうかを示す
isPending
フラグ。 - アクションを作成するための
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
を使用することで、トランジションが進行中の間にも即時のフィードバックを提供することができます。
例 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> ); }
ローディングインジケータを表示するためにタブのコンテナ全体が隠れることは不快なユーザ体験となってしまいます。TabButton
に useTransition
を追加すると、代わりにタブボタン内に保留状態を表示することができます。
“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> ); }
サスペンス対応ルータの構築
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 つの選択肢があります:
- 入力フィールド用の state(常に同期的に更新される)と、トランジションで更新する state を別々に宣言する。これにより、同期的な state を使用して入力フィールドを制御しつつ、トランジション state 変数(入力欄より「遅れる」ことになる)をレンダーロジックの残りの部分に渡すことができます。
- あるいは、保持する 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>
アクション のような高レベルの抽象化を提供しており、順序の管理を自動化します。高度なユースケースでは、独自のキューイングや中断ロジックを実装して、実行順序を管理する必要があります。