React 18 で追加されたSuspense
とuseTransition()
についてです。
コードだけでは分かりづらいし試すのも面倒だったので実行結果付きで載せてます。
実行結果を先に見たい人はスクロールしてください。
Suspense
まずはSuspenseを試します。
Service.ts
というサービスへのアクセスを模擬したファイルを用意します(3秒ラグを模擬します。)。
YukkuriComponent.tsx
とKossoriComponent.tsx
という二つのコンポーネントを用意
SuspenseComponent.tsx
はYukkuriComponent
とKossoriComponent
を切り替える親コンポネントです。
Service.ts
const sleep = (timespan: number) =>
{
return new Promise<void>(r => setTimeout(r, timespan));
}
export const loadAbcdItems: () => Promise<string[]> = async () =>
{
await sleep(3000);
return ['AAA', 'BBB', 'CCC', 'DDD'];
}
export const loadXyzItems: () => Promise<string[]> = async () =>
{
await sleep(3000);
return ['XXX', 'YYY', 'ZZZ'];
}
loadAbcdItems()
とloadXyzItems()
はそれぞれデータを取得するのに3秒掛かるようになってます。
YukkuriComponent.tsx と KossoriComponent.tsx
import { loadAbcdItems } from "./Service";
let items: string[] = [];
export const YukkuriComponent = () =>
{
if(items.length)
{
return (
<ul>
{
items.map(_ => <li key={_}>{_}</li>)
}
</ul>
)
}
throw new Promise<void>(async r => {
items = await loadAbcdItems();
r();
});
}
export default YukkuriComponent;
console.log("Load Yukkuri Component");
import { loadXyzItems } from "./Service";
let items: string[] = [];
export const KossoriComponent = () =>
{
if(items.length)
{
return (
<ul>
{
items.map(_ => <li key={_}>{_}</li>)
}
</ul>
)
}
throw new Promise<void>(async r => {
items = await loadXyzItems();
r();
});
}
export default KossoriComponent;
console.log("Load Kossori Component");
この二つのコンポーネントはほとんど同じことをしています。
YukkuriComponentはloadAbcdItems()を表示します。
KossoriComponentはloadXyzItems()を表示します。
変わったところは、まだデータが取得されていない状態ではPromise
をスローするということです。
loadAbcdItems() / loadXyzItems()
が終了したら再びレンダリングされ、items
の一覧が表示されます。
条件にitems.length
が0
の時は例外をスローしてますが、実際にはitemsが0でもいい場合を考慮してません。
その場合はバグになるので適当にnull判定をするなど対策してください。
SuspenseComponent.tsx
import React, { Suspense, useState } from "react";
const YukkuriComponent = React.lazy(() => import('./YukkuriComponent'));
const KossoriComponent = React.lazy(() => import('./KossoriComponent'));
const Loading = () =>
{
return <p>Loading...</p>
}
const SuspenseComponent = () =>
{
const [flag, setFlag] = useState(true);
return (
<>
<input type="button" value="change" onClick={e => setFlag(!flag)} />
<Suspense fallback={<Loading />}>
{
flag ? <YukkuriComponent /> : <KossoriComponent />
}
</Suspense>
</>
)
}
export default SuspenseComponent;
loadAbcdItems() / loadXyzItems()
が実行中の間Suspense
コンポーネントのfallback
で指定したコンポーネントが表示されます。
実行結果。
実行結果は次のようになります。
ページを更新するとLoading...
が表示されたあとloadAbcdItems()
の結果が表示されます。
change
ボタンを押すと再びLoading...
が表示された後loadXyzItems()
が表示されます。
ページの更新を確認するためにCodeSandboxの回転矢印をクリックして更新(Reflesh preview)してみてください。
useTransition()
変化をわかりやすくするため先に結果を表示します。
change
をクリックすると何かがかわってます。
クリックしてもLoading...
は表示されずYukkuriComponent
が表示され続け、およそ3秒後KossoriComponent
が表示されました。
変更箇所はSuspenseComponent.tsx
です。
import React, { Suspense, useState, useTransition } from "react";
const YukkuriComponent = React.lazy(() => import("./YukkuriComponent"));
const KossoriComponent = React.lazy(() => import("./KossoriComponent"));
const Loading = () => {
return <p>Loading...</p>;
};
const SuspenseComponent = () => {
const [flag, setFlag] = useState(true);
const [isPending, startTransition] = useTransition();
return (
<>
<p>{isPending ? "pending pending pending" : "yukkuri kossori change"}</p>
<input
type="button"
value="change"
onClick={(e) => startTransition(() => setFlag(!flag))}
/>
<Suspense fallback={<Loading />}>
{flag ? <YukkuriComponent /> : <KossoriComponent />}
</Suspense>
</>
);
};
export default SuspenseComponent;
変更部分は次の2箇所です
{isPending ? "pending pending pending" : "yukkuri kossori change"}
onClick={(e) => startTransition(() => setFlag(!flag))
状態の変更をstartTransition()
のコールバック内で行ってます。
KossoriComponent
が描画出来るまで現在のDOMが表示されるようになります。
isPending
は次が描画出来るまでtrue
になります。