useTransition
useTransition
は、UI をブロックせずに state を更新するための React フックです。
const [isPending, startTransition] = useTransition()
リファレンス
useTransition()
コンポーネントのトップレベルで useTransition
を呼び出し、state 更新の一部をトランジションとしてマークします。
import { useTransition } from 'react';
function TabContainer() {
const [isPending, startTransition] = useTransition();
// ...
}
引数
useTransition
には引数はありません。
返り値
useTransition
は常に 2 つの要素を含む配列を返します。
- トランジションが保留中であるかどうかを示す
isPending
フラグ。 - state 更新をトランジションとしてマークするための
startTransition
関数。
startTransition
関数
useTransition
によって返される startTransition
関数により、ある state 更新をトランジションとしてマークすることができます。
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ...
}
引数
scope
: 1 つ以上のset
関数を呼び出して state を更新する関数。React は引数なしで直ちにscope
を呼び出し、scope
関数呼び出し中に同期的にスケジュールされたすべての state 更新をトランジションとしてマークします。このような更新はノンブロッキングになり、不要なローディングインジケータを表示しないようになります。
返り値
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 つの項目を含む配列を返します:
- トランジションが保留中であるかどうかを示す
isPending
フラグ。 - state 更新をトランジションとしてマークするための
startTransition
関数。
その後、次のようにして state 更新をトランジションとしてマークできます。
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ...
}
トランジションを使用することで、遅いデバイスでもユーザインターフェースの更新をレスポンシブに保つことができます。
トランジションを使用すると、再レンダーの途中でも UI がレスポンシブに保たれます。例えば、ユーザがタブをクリックしたが、その後気が変わって別のタブをクリックする場合、最初の再レンダーが終了するのを待つことなくそれを行うことができます。
例 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> ); }
サスペンス対応ルータの構築
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>; }
エラーバウンダリでユーザにエラーを表示する
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);
同様に、以下のように更新をトランジションとしてマークすることはできません。
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 ...
}
}