useTransition
は、UI の一部をバックグラウンドでレンダリングできる React Hook です。
const [isPending, startTransition] = useTransition()
リファレンス
useTransition()
コンポーネントのトップレベルでuseTransition
を呼び出して、いくつかの状態更新をトランジションとしてマークします。
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
関数を呼び出すことによって、いくつかの状態を更新する関数。React は、パラメータなしでaction
をすぐに呼び出し、action
関数呼び出し中に同期的にスケジュールされたすべての状態更新をトランジションとしてマークします。action
で await された非同期呼び出しはトランジションに含まれますが、現在、await
後のset
関数を追加のstartTransition
でラップする必要があります(トラブルシューティング を参照)。トランジションとしてマークされた状態更新は、ブロッキングされず、不要なローディングインジケータを表示しません。
戻り値 ...
startTransition
は何も返しません。
注意事項 ...
-
useTransition
はフックであるため、コンポーネントまたはカスタムフック内でのみ呼び出すことができます。他の場所(たとえば、データライブラリから)でトランジションを開始する必要がある場合は、スタンドアロンのstartTransition
を代わりに呼び出してください。 -
更新をトランジションにラップできるのは、その状態の
set
関数にアクセスできる場合のみです。プロップまたはカスタムフックの値に応じてトランジションを開始する場合は、代わりにuseDeferredValue
を試してください。 -
startTransition
に渡す関数はすぐに呼び出され、実行中に発生するすべての状態更新がトランジションとしてマークされます。たとえば、setTimeout
で状態更新を実行しようとすると、トランジションとしてマークされません。 -
非同期リクエスト後の状態更新をトランジションとしてマークするには、別の
startTransition
でラップする必要があります。これは既知の制限であり、将来修正される予定です(トラブルシューティング を参照)。 -
startTransition
関数は安定したアイデンティティを持っているため、Effect の依存関係から省略されることがよくありますが、含めても Effect が起動されることはありません。リンターがエラーなしで依存関係を省略できる場合は、省略しても安全です。Effect の依存関係の削除についてもっと学ぶ。 -
トランジションとしてマークされた状態更新は、他の状態更新によって中断されます。たとえば、トランジション内でチャートコンポーネントを更新したが、チャートの再レンダリング中に input に入力し始めた場合、React は input の更新を処理した後にチャートコンポーネントのレンダリング作業を再開します。
-
トランジションの更新を使用してテキスト入力を制御することはできません。
-
複数のトランジションが進行中の場合、React は現在それらをまとめてバッチ処理します。これは、将来のリリースで削除される可能性のある制限です。
使用方法 ...
アクションによるノンブロッキング更新の実行
コンポーネントの先頭で`useTransition`を呼び出してアクションを作成し、保留状態にアクセスします。
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`に渡された関数は「アクション」と呼ばれます。アクション内で状態を更新し(オプションで)副作用を実行できます。この作業は、ページ上のユーザーインタラクションをブロックすることなくバックグラウンドで実行されます。1つの遷移には複数のアクションを含めることができ、遷移が進行中の間、UIは応答性を維持します。たとえば、ユーザーがタブをクリックした後、考えを変えて別のタブをクリックした場合、最初の更新が完了するのを待たずに2回目のクリックがすぐに処理されます。
進行中の遷移に関するフィードバックをユーザーに提供するために、`isPending`状態は、`startTransition`の最初の呼び出しで`true`に切り替わり、すべてのアクションが完了し、最終状態がユーザーに表示されるまで`true`のままになります。不要なローディングインジケーターを防ぐために、アクションの副作用が確実に順番に完了するように遷移が行われ、`useOptimistic`を使用して遷移の進行中に即時フィードバックを提供できます。
例 1(続き) 2: アクションで数量を更新する
この例では、`updateQuantity`関数は、カート内のアイテムの数量を更新するためのサーバーへのリクエストをシミュレートしています。この関数は、リクエストの完了に少なくとも1秒かかるように*意図的に遅延*させています。
数量を複数回すばやく更新します。リクエストが進行中は保留中の「合計」状態が表示され、「合計」は最後のリクエストが完了した後にのみ更新されることに注意してください。更新はアクション内で行われるため、リクエストの進行中に「数量」を更新し続けることができます。
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は次のような組み込みの抽象化を提供します。
これらのソリューションは、リクエストの順序を自動的に処理します。遷移を使用して非同期状態遷移を管理する独自の カスタムフックまたはライブラリを構築する場合、リクエストの順序をより詳細に制御できますが、自分で処理する必要があります。
コンポーネントから`action`プロパティを公開する
親がアクションを呼び出せるように、コンポーネントから`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>
);
}
親コンポーネントは`action`内で状態を更新するため、その状態更新は遷移としてマークされます。つまり、「投稿」をクリックしてからすぐに「連絡先」をクリックしても、ユーザーインタラクションはブロックされません。
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`ブール値を使用して、遷移が進行中であることをユーザーに示すことができます。たとえば、タブボタンに特別な「保留中」の視覚状態を持たせることができます。
function TabButton({ action, children, isActive }) {
const [isPending, startTransition] = useTransition();
// ...
if (isPending) {
return <b className="pending">{children}</b>;
}
// ...
「投稿」をクリックすると、タブボタン自体がすぐに更新されるため、より反応が良くなったことがわかります。
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 を使用してデータを取得します。「投稿」タブをクリックすると、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
を追加すると、代わりにタブボタンに保留状態を表示できます。
「投稿」をクリックしても、タブコンテナ全体がスピナーに置き換えられなくなることに注意してください。
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> ); }
Suspense でのトランジションの使用について詳しくは、こちらをご覧ください。
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> ); }
トラブルシューティング
トランジションでの入力の更新が機能しない
入力を制御する状態変数にはトランジションを使用できません。
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} />;
これは、トランジションはブロッキングされないためですが、変更イベントに応答して入力を更新することは同期的に行われる必要があります。入力が行われたことに応答してトランジションを実行したい場合は、2つのオプションがあります。
- 2つの別々の状態変数を宣言できます。1つは入力状態用(常に同期的に更新されます)、もう1つはトランジションで更新する変数です。これにより、同期状態を使用して入力を制御し、トランジション状態変数(入力の「遅延」)をレンダリングロジックの残りの部分に渡すことができます。
- または、1つの状態変数を使用し、実際の値の「遅延」となる
useDeferredValue
を追加することもできます。新しい値に「追いつく」ために、ブロッキングされない再レンダリングが自動的にトリガーされます。
React は私の状態更新をトランジションとして扱わない
状態更新をトランジションでラップする場合は、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);
React は await
後の状態更新をトランジションとして扱わない
startTransition
関数内でawait
を使用すると、await
後に発生する状態の更新はトランジションとしてマークされません。それぞれのawait
の後で、状態の更新をstartTransition
呼び出しでラップする必要があります。
startTransition(async () => {
await someAsyncFunction();
// ❌ Not using startTransition after await
setPage('/about');
});
ただし、これは代わりに機能します。
startTransition(async () => {
await someAsyncFunction();
// ✅ Using startTransition *after* await
startTransition(() => {
setPage('/about');
});
});
これは、Reactが非同期コンテキストのスコープを失うことによるJavaScriptの制限です。将来的にAsyncContextが利用可能になると、この制限はなくなります。
コンポーネントの外部からuseTransition
を呼び出したいです
useTransition
はHookであるため、コンポーネントの外部から呼び出すことはできません。この場合は、代わりにスタンドアロンのstartTransition
メソッドを使用してください。動作は同じですが、isPending
インジケーターは提供されません。
startTransition
に渡す関数はすぐに実行されます...
このコードを実行すると、1、2、3が出力されます。
console.log(1);
startTransition(() => {
console.log(2);
setPage('/about');
});
console.log(3);
**1、2、3が出力される予定です。** startTransition
に渡す関数は遅延されません。ブラウザのsetTimeout
とは異なり、後でコールバックを実行しません。Reactは関数をすぐに実行しますが、実行中にスケジュールされた状態の更新はトランジションとしてマークされます。これは次のように動作すると想像できます。
// 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 ...
}
}
トランジションでの状態の更新の順序が正しくありません...
startTransition
内でawait
を使用すると、更新が順不同で発生することがあります。
この例では、updateQuantity
関数は、カート内のアイテムの数量を更新するためのサーバーへのリクエストをシミュレートしています。この関数は、ネットワークリクエストの競合状態をシミュレートするために、*意図的に1つおきのリクエストを前のリクエストの後に返します*。
数量を1回更新してから、複数回すばやく更新してみてください。合計が正しくない場合があります。
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>
アクションなどのより高レベルの抽象化を提供します。高度なユースケースでは、これを処理するために独自のキューイングと中止ロジックを実装する必要があります。