🎛️

Code Splittingによるimportエラーをいい感じに処理する

2025/02/20に公開

Code Splittingとは

「SPAのassetを最初に全部取ってきちゃうと初期ロードがデカくなっちゃうので、よしなな単位で分割して、必要なタイミングでlazy loadしよう」みたいなやつです。
今回の記事はページコンポーネントごとにコード分割する前提で書いてます。

const Home = React.lazy(() => import("./routes/home"));
const About = React.lazy(() => import("./routes/about");

https://react.dev/learn/build-a-react-app-from-scratch#code-splitting

つらみ

このままでは、時折以下のようなエラーが起きてしまうかと思います。

TypeError: Failed to fetch dynamically imported module:
https://example.com/assets/index-asdf1234.js

Vite等ではindex-asdf1234.jsのように、ビルド成果物にhashがついており、デプロイのたびにjsファイル名が更新されます。
そのため、デプロイの前後でWebアプリに滞在したままの状態でdynamic importが実行されると、上記のようなruntimeのエラーを投げてしまいます。
詳しい説明はこちらの記事に頼らせてください。

https://qiita.com/KokiSakano/items/554693c65890d22b0284

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