注意: これは実験的な機能なので、v17リリースまでにAPIが変更される可能性が高い。紹介する限り既に unstable_AsyncMode や resource.read のAPIは一度変更されている。
標準版 React の canary ビルドを入れる
yarn add react@canary react-dom@canary
import "@babel/polyfill";
import React from "react";
import ReactDOM from "react-dom";
import { createResource, createCache } from "simple-cache-provider";
// cast any for typescript
const Timeout: React.ComponentType<{ ms: number }> = (React as any).Timeout;
const AsyncMode = (React as any).unstable_AsyncMode;
const cache = createCache();
const hn = createResource(async (id: number) => {
const url = `https://hacker-news.firebaseio.com/v0/item/${id}.json`;
const res = await fetch(url);
await new Promise(resolve => setTimeout(resolve, 400));
return res.json();
});
const HNStory = (props: { id: number }) => {
const data = hn.read(cache, props.id);
return (
<p>
{data.title}: {data.url}
</p>
);
};
class App extends React.PureComponent {
render() {
return (
<div>
<h1>Try suspense</h1>
<button
onClick={() => {
this.forceUpdate();
}}
>
reload
</button>
<hr />
<Timeout ms={200}>
{(didTimeout: boolean) => {
if (didTimeout) {
return <span>The content is still loading :(</span>;
}
return <HNStory id={8863} />;
}}
</Timeout>
</div>
);
}
}
ReactDOM.render(
<AsyncMode>
<App />
</AsyncMode>,
document.querySelector(".root")
);
挙動
一旦は HNHistory のレンダリングはスキップされる。Hacker News の API 叩く。200ms を超えると Timeout children に与えた renderProp が didMount: boolean
の情報を付与して発火し、プレースホルダが表示される。それまでに取得が完了されていれば、そのまま表示される。
reload のボタンを押すと強制リロードされるが、キャッシュ構築済みなのでプレースホルダが出ることなく表示される。
AsyncMode を外した場合、初回レンダリング時から Timeout の children が発火する。
解説
- AsyncMode でラップすることで、非同期レンダリングを有効化する
- simple-cache-provider によって、cache と resource を生成する。
- これは単にリクエスト時の引数に対してその結果をキャッシュするモジェール。なので simple-cache
-
resource.read(cache, key)
時、キャッシュが無ければthrow new Promise(...)
で例外処理に飛ぶ - Timeout に実装された ErrorBoundary によって、catch される https://reactjs.org/docs/error-boundaries.html
-
resource.read(cache, key)
時、キャッシュが構築されていれば動機的に返却する - 正常処理を行う
Reactの render は同期でなければいけない、という縛りを、見た目上同期APIに見えつつ Promise を throw して ErrorBoundary で catch してフォールバックすることで実現している。なので、v17以降では、 render の処理は最後まで走らないことがある というのを認識しないといけない。さすがに、ここで副作用を起こすコードを書く人はいないはずだが…。
要は、Reactの世界観の中では、ライブラリ作者はレンダリングの優先度付けのために、自分で componentDidCatch を実装したカスタムな ErrorBoundary を使って、その復帰処理で好き勝手してもよいということになる。
ライブラリ作者は、行儀よくライブラリの邪魔をしないErrorを裏で生成しないといけない。うまくやらないと治安悪いライブラリ増えそうな気がする。