📘

【React】named exportのlazy importを綺麗に行う

2024/07/14に公開
1

Reactのコンポーネントを遅延読み込みする場合に使用するReact.lazy

import { lazy, Suspense, useState } from 'react';

const MarkdownPreview = lazy(() => import("./MarkdownPreview"));

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <div>
      <button onClick={() => setShow(true)}>show</button>
      <Suspense>
        {show && <MarkdownPreview />}
      </Suspense>
    </div>
  );
}

https://react.dev/reference/react/lazy

非同期関数であるimportを内部でよしなにしてくれ、コンポーネントを遅延読み込みすることを可能にしてくれます。
React.useに似ていますね。
if文で分岐しているなどで初期描画時に不要な依存がある場合に、コンポーネントが必要になるまで読み込みを延期することでパフォーマンスを向上させることができます。
このlazyはdefault exportを想定しており、そのままではnamed exportされたコンポーネントに対しては使用できず、ひと工夫が必要です。

個別に対応する方法

以下の.thenによる即時の定型文を書くことで個別にimportを解決することは可能です。

import { lazy } from "react";

const SomeComponent = lazy(() => import("./foo").then((module) => ({ default: module.SomeComponent })));

やっていることは、importしたモジュールの中から希望のexportを選択してそれをdefault exportに変換しているだけです。
ただしこれを毎回書くのは面倒です。

Utility関数を作成して再利用する方法

https://qiita.com/KokiSakano/items/b6d4e6875443064032b4

こちらの記事で紹介されていた方法です。
util関数を作成しておくことでnamed exportをこの関数に任せることができます。

import { lazy, type ComponentType } from "react";

export const lazyImport = <T extends { [P in U]: ComponentType<any> }, U extends string>(
  factory: () => Promise<T>,
  name: U,
) => ({
  [name]: lazy(() => factory().then((module) => ({ default: module[name] }))),
});
使用例
const { SomeComponent } = lazyImport(() => import("./foo"), "SomeComponent");

左辺で名前を指定しているにも関わらず、右辺にも名前を指定する必要があるのが少し気になります。
配列を受け入れていないため、複数のコンポーネントを同一のモジュールからまとめてインポートすることはできないようです。

react-lazilyを使用する方法

良い方法は無いものかと探していたところ、以下のライブラリを見つけました。
ライブラリをインストールすることが許されている場合、react-lazilyを使用することでnamed exportを綺麗にimportすることができます。
https://www.npmjs.com/package/react-lazily

ライブラリとはいうものの関数単体で268byteと軽量なので、依存を増やすことの罪悪感も少ないです。

import { lazily } from "react-lazily";

const { SomeComponent } = lazily(() => import("./foo"))

通常のReact.lazyと同様の使用感で、named exportされたコンポーネントをそのまま使用することができます。

複数のコンポーネントを単一のインポートで型補完を効かせながらインポートする図

このように複数のコンポーネントを単一のインポートで型補完を効かせながら遅延インポートすることができます。
型定義がLazyExoticComponentにならないところも気に入っています。
記載したJSDocコメントが失われないため、Propsの指定の際も記載したコメントが残ります。

考えてみれば当然のことではありますが、同一のモジュールからインポートした複数のコンポーネントを別のSuspense境界で使用する場合、両方がSuspendされる点は注意が必要です。
同一のSuspense内であれば問題ありません。
以下のリポジトリで念の為雑に検証しました。
ネットワークタブを見ると、両方のコンポーネントが含まれるソースが同時に読み込まれていることが確認できます。
また、片方のコンポーネントが読み込まれると両方がSuspendされています。
https://bmthd.github.io/lazy-import/
ソースコード

実装を見に行ったところProxyと、asによる型キャストで実装されていました。

https://github.com/JLarky/react-lazily/blob/main/src/core/lazily.ts

これは思いつかないですね…!

私は使用していませんが、サーバーフレームワークのSSR向けに@loadable/componentというライブラリがあるようで、これとの併用可能なAPIもあるようです。

まとめ

React.lazyを使用する際にnamed exportを綺麗にlazy importする方法を紹介しました。
react-lazilyは軽量で使いやすいので、lazy importを使用する際にはぜひ使用してみてください。

Discussion

glassonion1glassonion1

ライブラリの遅延読み込みで悩んでたのでめちゃくちゃ参考になりました。ありがとうございます!