React SuspenseとErrorBoundaryどう使うか
以下の環境でSuspenseとErrorBoundaryの使い方を模索する
- React v18
- MUI v5
- @tanstack/react-query v4
- orval v6
ErrorBoundaryは少なくとも https://github.com/bvaughn/react-error-boundary を導入するのは確定して良さそう。
シンプルにSuspenseモードをSuspenseで利用するといわゆるFetch-on-render が提供できる (これはSuspenseを使わないでも同じ)
const Component = () => {
const result = useData({suspense: true});
return <div>{result.data?.foo}</div>
}
<Suspense fallback={<div>Loading</div>}>
<Component/>
</Suspense>
https://eh-career.com/engineerhub/entry/2023/07/14/093000 でも記述があるとおり ↑のresult.dataの型がoptionalになる問題はある。react-queryのv5からはuseSuspenseQueryとなってoptionalではないデータが取れそう。
ただし、orvalによる対応がどうなるかわからないので利用可能かは疑問。
Suspense & fetchをネストさせた場合にウォーターフォール的になるのか。なるならfetch位置の妥当性は... (これはSuspense関係ないか)
理屈から言えばpending (例外) 位置より後ろは評価されないからpending解除されないと下流は流れない気がする
エラー時の処理はhooks側(onError)に寄せるか、コンポーネント(ErrorBoundary)に寄せるか。
Suspenseはmutation目的ではないので、useQueryが基本となる。
useQueryではisErrorによるコンポーネントでの制御を期待しているように見える。(v4ではonErrorがdeprecated)
あたりまえの話ではあるけどqueryとmutationを分けましょう、が前提になる。
fetch(リポジトリ)まわり & ライブラリのおかげで同じようなI/Fになってるけど、Suspenseやエラー制御という観点では完全に別物。
というわけで、まずはquery系についてだけ整理。
要するにこれか・・・
ある程度ここをパターン化したいんだ。
あらゆるコンポーネントの周りにサスペンスバウンダリを置こうとしないようにしてください。ユーザに見せたいロードの各ステップよりもサスペンスバウンダリを細かく設置すべきではありません。デザイナと一緒に作業している場合は、ロード中表示をどこに配置するべきか尋ねてみてください。おそらく、それはデザインの枠組みにすでに含まれているでしょう。
すべてのコンポーネントを別々のエラーバウンダリでラップする必要はありません。エラーバウンダリの粒度について考える際は、エラーメッセージをどこに表示するのが理にかなっているかを考えてみてください。例えば、メッセージングアプリでは、会話のリストをエラーバウンダリで囲むのが理にかなっています。また、メッセージを個別に囲むことも理にかなっているでしょう。しかし、アバターを 1 つずつ囲むことには意味がありません。
useQuery({suspense:true})
をSuspense対応hooksとすると、ある画面においてこのhooksを使ったときの表現したいローディング状態(表現) はまとまっているほうが良さそう。(XXX画面におけるローディング制御責務)
useSuspenseみたいな機能がない以上、この機能はコンポーネントとして提供するのがいいのかな。
Result型もSuspense型も結局I/Fまわりは変わらない
Result型
const SomeComponent = ({ data }) => {
return <div>{data}</div>;
};
const result = useSomeQuery();
switch (result.status) {
case 'loading':
return <Loading />;
case 'error':
return <Error />;
case 'success':
return <SomeComponent data={result.data} />;
}
厳密にはこちらは Fetch-on-render
じゃないのか。
Suspense型
const SomeComponent = () => {
const { data } = useSomeQuery({ suspense: true });
return <div>{data}</div>;
};
<Suspense fallback={<Loading />}>
<ErrorBoundary fallback={<Error />}>
<SomeComponent />
</ErrorBoundary>
</Suspense>;
Suspense型はBoundaryとして状態をまとめられる。それはそう。
小さいローディングがあちこち発生するのを防げるというメリット。
エラーをoptoinalにしつつ、useSomeQueryに沿った型が強制される感じがいいのかな。