React + SWR、ケース別にエラーハンドリングしたい
Webアプリケーションで発生しうるエラーをざっくり分けると
- 予期されたエラー
- ユーザー操作に対する同期的なエラー(基本的にdisabledで対処すべきとかはさておき)
- 非同期処理によるエラー
- ライブラリから投げられるエラー
- 予期せぬエラー
- 通信失敗、404アクセス、混雑などそもそも鯖に到達しないエラー
- クライアントサイドでのクラッシュ
予期せぬエラーに対してはErrorBoundaryやカスタムエラーページで対応するとして、本題は予期されたエラーをどうハンドリングしていくか。
かつては中央集権的に、たとえばAxiosInterceptorだったりthrow APP_ERROR['Foo'] といった手法で巨大なErrorHandlerクラスやhooksに対応を投げていた。
しかしこのやり方には以下のような問題があった。
- throwされたエラーを受け取る箇所では非同期も同期もまとめて処理していた
- 異なるAPI/同じAPIだが異なるケースで同じエラーが吐かれたとき区別することが困難で、別途catchが必要になりチグハグであった
- ページやhooksは正常系にのみ関心を持っていたが、異常系の処理が重要なケースであっても不意に分離されてしまっていた
そのため、予期可能な異常系が存在する操作で
// 同期
const handleClick = (event) => {
if (event.target....) {
handleFooError(event);
return;
};
// 以降正常処理
};
// 非同期
const {data, error} = useSWR(...);
// ライブラリ
player.on('error', (error) => {
handlePlayerError(error);
});
といった方針を考えてみる。
このような方針にすることによって
同期エラー
メリットが大きい。UIに影響するケースが多いのでエラー発生状況とリアクションが独特になりやすい。
非同期エラー
実態はほぼ変わらないが「中央集権的だが例外はある」から「個別に処理するが概ね共通のものを呼び出す」になる。SWRの {data, error}
で無理なく実装できそうだが
const {data, error} = useSWR(path, fetcher, { onError: handleSwrError });
// 原則ここにuseEffectを使わないけど
useEffect(() => error && handleSwrError(error), []);
はuseSWRのonErrorが優先して両方発火する。
ここら辺どういった作法で書いていくべきかはあとで考える。
ライブラリエラー
もっとも影響が少ないかも。
大抵throwしてくれてるのを改めてライブラリ使用側で拾うが、共通のErrorHandlerで拾うか使用しているまさにその付近で拾うかの違いでしかない。この違いが大きいケースもあるかもしれないが、その場合はもともとライブラリに搭載されているエラー処理に従うケースが多かった(?)
以降は非同期エラーのハンドリング、特にSWR利用時について考える。
SWRに備わっているエラーハンドリングの手法は以下の3つ。
// 1. SWRConfig
<SWRConfig onError={handleError}>{children}</SWRConfig>
// 2. useSWR onError
const { data } = useSWR(path, fetcher, { onError: handleError })
// 3. useSWR returned error
const { data, error } = useSWR(path, fetcher);
1は今回の主旨から外れる(共通エラーがあるならhandleErrorといった処理関数側に閉ざす)ため2, 3を考慮したい。おそらく
// A. 引数でハンドラーを呼ぶ
const { data } = useFetchHistory(handleFetchHistoryError);
// B. hooksに閉ざす
const { data } = useFetchHistory(); // 中でonErrorを定義してる
// C. errorを出してhooksを読んだ側で処理させる
const { data: history, error } = useFetchHistory();
useHandleHistoryError(error);
return (
<section>
<h2>なんとか履歴</h2>
<Suspense fallback={<SkeltonList />}>
<HistoryList {...history} />
</Suspense>
</section>
)
「同じAPI呼んで同じエラー出たけど、呼んだケースによってエラーを分けたい等」はhooks自体を分けてしまえばいい。
非同期のエラー処理は非同期処理の1つであるからuseSWRを呼ぶhooksの責務(?)
useFetchHistory()
useFetchLimitedHistory()
==================
export function useFetchHistory() {
const { handleApiError } = useApiError();
const handleError = (error) => {
if (error...) { /** 専用の処理 */ return; };
handleApiError(error);
}
const { data } = useSWR(path, fetcher, { onError: handleError })
return { data }
}
ひとまずこの方針でやってみて、今までの辛さが解消されるか、別の辛さが生まれるかなど追って記述する。
稀にあるuseSWRのmutateを使いたくいケースで個別の対応が必要になる……けどそれくらいかも。
おおむね満足してる