startTransitionとは
startTransitionは、ユーザーがウェブページ上で操作しているときにスムーズな動作を保つための仕組みです。
通常、ウェブページ上で何か操作をすると、その操作に関連する部分が再レンダリングされます。再レンダリングには時間がかかる場合や、他の操作と競合して動作が遅くなる場合があります。
startTransitionを使用すると、操作に関連する再レンダリングを、ユーザーの操作を妨げないように遅延させることができます。つまり、ユーザーがスムーズに操作できるようになります。
使い方
import { startTransition } from "react";
startTransition(() => {
setSomeState(/* ... */);
});
startTransitionはコールバック関数を受け取り、その関数をすぐに呼び出します。
startTransitionの中で行われたステート更新はトランジションとして扱われます。
トランジションは、優先度の低いステート更新を意味します。
▼ startTransitionなし
function App() {
const [counter, setCounter] = useState(0);
return (
<div className="text-center">
<h1 className="text-2xl">React App!</h1>
<Suspense fallback={<p>Loading...</p>}>
<ShowData dataKey={counter} />
</Suspense>
<p>
<button
className="border p-1"
onClick={() => {
setCounter(counter + 1);
}}
>
startTransitionなし {counter}
</button>
</p>
</div>
);
}
▼ startTransitionあり
function App() {
const [counter, setCounter] = useState(0);
return (
<div className="text-center">
<h1 className="text-2xl">React App!</h1>
<Suspense fallback={<p>Loading...</p>}>
<ShowData dataKey={counter} />
</Suspense>
<p>
<button
className="border p-1"
onClick={() => {
startTransition(() => {
setCounter(counter + 1);
});
}}
>
startTransitionあり {counter}
</button>
</p>
</div>
);
}
トランジションがない場合、ス
テート更新後に即座に結果が描画されます。
トランジションがある場合、サスペンスの部分をフォールバックせずに古い表示のまま残します。
全ての更新が終わったら描画されます。
↓
トランジションに関わるレンダリングの挙動は、レンダリングの一貫性が保証されるということを意味します。
裏の世界
<button
className="border p-1"
onClick={() => {
startTransition(() => {
setCounter((c) => c + 1); // ボタンを連打した時の挙動が変わる
});
}}
>
Counter is {counter}
</button>
setCounter((c) => c + 1);
「Counter is 0」の状態から10回ボタンを押した場合、次に画面が更新されたときはいきなり「Counter is 10」と表示されます。
レンダリングの一貫性のため、ShowDataに表示されるのももちろん「Data for 10 is ...」です。
ボタンを押し続けた場合は、画面がずっと更新されず、最後にボタンを押してから1秒経ってやっとサスペンドが終了するという挙動になります。
画面にはずっとcounterが0の世界を表示し続けているにもかかわらず、裏ではcounterが1→2→3→……と変化している世界が存在しているということです。
裏の世界が表に出てくる(裏の世界のレンダリング結果が反映される)のは、裏の世界のレンダリングが完了した(=サスペンドが終了した)ときです。
表の世界と交差
表のrenderingは裏側にも反映される
- ReactのConcurrent Modeでは、裏の世界の更新が優先度の高い場合、まだ完了していない表の世界の更新が表示される。
- これは、ユーザーがスムーズな操作体験を得るための効果的な方法です。
- 裏の世界の更新が完了した後、Reactは裏の世界のレンダリング結果を表の世界に反映させます。
これにより、一貫した表示が提供されます。
表と裏の世界の交差により、Reactは高いパフォーマンスとスムーズなユーザー体験を実現することができます。
ステート更新が発生する瞬間を調べる
・裏の世界のステートというのは、表の世界のステートが更新された際には毎回新たに計算し直されている
<button
className="border p-1"
onClick={() => {
startTransition(() => {
setCounter((c) => {
console.log(c, "→", c + 1);
return c + 1;
});
});
}}
>
Counter is {counter}
</button>
- 裏の世界のステートは、表の世界のステートが更新されるたびに新たに計算されます。
- Reactは、裏の世界のステートと表の世界のステートの差分を覚えておきます。
- 表の世界が更新されると、Reactは差分を再適用して裏の世界のステートを取得します。
- これにより、裏の世界のステートは表の世界と同期されます。
- 差分を再適用することで、裏の世界のステートを更新することができます。
useTransition を使用する
useTransition
-今の画面を残しつつ次の画面を裏でレンダリングすることを可能にするフック
startTransition関数というのは普通にreactからインポートできるものと同じ効果を持ち、トランジションを開始することができます。そして、そのトランジションの最中(裏の世界が存在している間)、同じuseTransitionフックから返されるisPendingフラグが表の世界ではtrueになります。
import { useTransition } from
const [isPending, startTransition] = useTransition();
import { Suspense, useState, useTransition } from "react";
import "./App.css";
import { ShowData } from "./components/ShowData";
import { useTime } from "./hooks/useTimes";
function App() {
const [counter, setCounter] = useState(0);
const time = useTime();
const [isPending, startTransition] = useTransition();
return (
<div className="text-center">
<h1 className="text-2xl">React App!</h1>
<p className={"tabular-nums" + (isPending ? " text-blue-700" : "")}>
🕒 {time}
</p>
<Suspense fallback={<p>Loading...</p>}>
<ShowData dataKey={counter} />
</Suspense>
<p>
<button
className="border p-1"
onClick={() => {
startTransition(() => {
setCounter((c) => c + 1);
});
}}
>
Counter is {counter}
</button>
</p>
</div>
);
}
※ startTransitionはreactからインポートしたものではなく、useTransitionフックから返ってきたもの
useTransitionから返ってくるstartTransition関数はそのトランジションを開始する関数であり、isPendingフラグはそのトランジションが実行中かどうかを表すフラグ
※ ReactからデフォルトでエクスポートされているstartTransitionは、Reactがあらかじめ用意してくれた、言わば“デフォルトトランジション”を実行するもの
useTransitionを二つ定義してみる
function App() {
const [counter, setCounter] = useState(0);
const time = useTime();
const [isPending, startTransition] = useTransition();
const [isPending2, startTransition2] = useTransition();
return (
<div className="text-center">
<h1 className="text-2xl">React App!</h1>
<p className={"tabular-nums" + (isPending ? " text-blue-700" : "")}>
🕒 {time}
</p>
<Suspense fallback={<p>Loading...</p>}>
<ShowData dataKey={counter} />
</Suspense>
<p>
<button
className="border p-1"
onClick={() => {
startTransition(() => {
setCounter((c) => c + 1);
});
startTransition2(() => {
setCounter((c) => c + 5);
});
}}
>
Counter is {counter}
</button>
</p>
</div>
);
}
上記のコードでは、ボタンがクリックされると、startTransitionとstartTransition2が同時に呼び出されています。このため、両方のトランジションが同時に実行されます。
各トランジションは別々の状態を更新し、それぞれの状態更新が非同期に処理されます。最終的には、setCounter((c) => c + 1)によってcounterが1増加し、setCounter((c) => c + 5)によってcounterがさらに5増加します。
画像のスクリーンキャストに表示されているように、ボタンをクリックするたびにトランジションが発生し、カウンターが適切に更新されます。
とてもわかりやすい記事です
https://zenn.dev/uhyo/books/react-concurrent-handson-2