Code Splittingによるimportエラーをいい感じに処理する
Code Splittingとは
「SPAのassetを最初に全部取ってきちゃうと初期ロードがデカくなっちゃうので、よしなな単位で分割して、必要なタイミングでlazy loadしよう」みたいなやつです。
今回の記事はページコンポーネントごとにコード分割する前提で書いてます。
const Home = React.lazy(() => import("./routes/home"));
const About = React.lazy(() => import("./routes/about");
つらみ
このままでは、時折以下のようなエラーが起きてしまうかと思います。
TypeError: Failed to fetch dynamically imported module:
https://example.com/assets/index-asdf1234.js
Vite等ではindex-asdf1234.js
のように、ビルド成果物にhashがついており、デプロイのたびにjsファイル名が更新されます。
そのため、デプロイの前後でWebアプリに滞在したままの状態でdynamic importが実行されると、上記のようなruntimeのエラーを投げてしまいます。
詳しい説明はこちらの記事に頼らせてください。
Webアプリの性質やデプロイのタイミングによっては、最悪このまま放置してしまってもいいかもしれませんが、高頻度で起きるとかなり体験が悪くなってしまうので、うまいこと対処したいです。
どう対処するか
キャッシュの設定をミスったり、そもそもimportするものを間違っていたりしていなければ、基本的には再読み込みで治るので、dynamic importのエラーを補足したら一度だけreloadする。それで駄目ならfallbackを表示するが落とし所かなと考えています。
ということで、サンプルコードがこちらです。
const RELOADED = "reloaded";
export const safeLazy = (
factory: () => Promise<{ default: React.FC }>,
): React.LazyExoticComponent<React.FC> =>
React.lazy(async () =>
factory()
.then((page) => {
// reset reload state when successfully imported
window.name = "";
return page;
})
.catch((e) => {
// reload only once
if (window.name !== RELOADED) {
window.location.reload();
window.name = RELOADED;
return {
default: () => {
// return loading component
return <div>Loading... </div>;
},
};
}
return {
default: () => {
// return fallback component
return <div>Failed to load page.</div>;
},
};
}),
);
import自体をwrapしてもいいですが、file pathの静的解析の恩恵を受けるために、React.lazyをラップしています。
React.lazyの定義が以下なので、これに合わせて書いています。
function lazy<T extends ComponentType<any>>(
load: () => Promise<{ default: T }>,
): LazyExoticComponent<T>;
また、reloadの制御のために雑にwindow.nameをstate代わりに利用していますが、url queryやhash、session storageを使うといった手もあります。
つかいかた
あとはReact.lazyをこんな感じで置き換えてやればOKです。今のところはこれでうまく動いてます。
「もっといい方法があるぜ!」とかあったら教えてくれると嬉しいです。
const Home = safeLazy(() => import("./routes/home"));
const About = safeLazy(() => import("./routes/about");
Discussion