かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

React.lazy でコンポーネントを Dynamic import してみる

環境 - react@18.1.0 - vite@2.9.9 - typescript@4.6.4

React.lazy

Reacy.lazy を使うと import() で読み込んだコンポーネントを通常のコンポーネントとして扱うことができる

下記の方法でコンポーネントを Dynamic import できる

// component
export default MyComponent: FC = () => {...};

// MyApp.tsx
const MyComponent = React.lazy(() => import('./path/to/MyComponent'));
// `<MyComponent />` として使用できる

React.lazy で Dynamic import したコンポーネント<Suspense> で囲う必要がある

遅延コンポーネントは、Suspense コンポーネント内でレンダーされる必要があります。これによって、遅延コンポーネントのローディングの待機中にフォールバック用のコンテンツ(ローディングインジケータなど)を表示できます。
cf. コード分割 – React

React.Susponse で囲ってないとエラーで React がクラッシュする

import 中にコンポーネントを表示しようとしてしまった場合エラーでアプリケーションがクラッシュします。動的な import なので常にクラッシュする訳ではないので気づきづらいので注意が必要です。

// MyComponent.tsx
export default MyComponent: FC = () => {...};

// MyApp.tsx
import { FC, lazy } from 'react';
const MyComponent = lazy(() => import('./MyComponent'));

const MyApp:FC = () => {
  return <MyComponent />
}

次のようなエラーが発生する

  • The above error occurred in one of your React components
  • Consider adding an error boundary to your tree to customize error handling behavior. Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.
  • A component suspended while responding to synchronous input. This will cause the UI to be replaced with a loading indicator. To fix, updates that suspend should be wrapped with startTransition.

👇 React.lazy で読み込んだコンポーネントは必ず <Suspense> で囲いインポート中の fallback を設定する

// MyApp.tsx
import { FC, lazy, Suspense } from 'react';
const MyComponent = lazy(() => import('./MyComponent'));

const MyApp:FC = () => {
  return (
    <Suspense fallback="loading…">
      <MyComponent />
    </Suspense>
  );
}

named export されたコンポーネントを読み込む方法

デフォルトでは React.lazy, import() を使った Dynamic import はデフォルトでは default import されたコンポーネントにしか対応されていません。

React.lazy は現在デフォルトエクスポートのみサポートしています。インポートしたいモジュールが名前付きエクスポートを使用している場合、それをデフォルトとして再エクスポートする中間モジュールを作成できます。これにより、tree shaking が機能し未使用のコンポーネントを取り込まず済むようにできます。
cf. コード分割 – React

1. default import する中間モジュールを別途作成する方法

公式に書かれている方法。named export されているコンポーネントを読み込み default import する中間もモジュールを作成し、それを Dynamic import する

Dynamic import したいコンポーネント

// ManyComponents.js
export const MyComponent = /* ... */;
export const MyUnusedComponent = /* ... */;

中間モジュール

// MyComponent.js
export { MyComponent as default } from "./ManyComponents.js";

中間モジュールを React.lazy で Dynamic import する

import { lazy } from 'react';
const MyComponent = lazy(() => import("./MyComponent.js"));

default import したオブジェクトの中から named export されたモジュールを default として返す

React.lazy の型はこんな風になっていました。

type LazyExoticComponent<T extends ComponentType<any>> = ExoticComponent<ComponentPropsWithRef<T>> & {
  readonly _result: T;
};

function lazy<T extends ComponentType<any>>(
  factory: () => Promise<{ default: T }>
): LazyExoticComponent<T>;

最終的に {default: ComponentType} の形が返却されれば React.lazy としては正確に動作しそうです。 React のコンポーネントは names export の場合も ESModule で export されていると思うので、全体を default インポートした Object には named export されたモジュールが含まれアクセスが可能なので、これを利用して default キーで必要なモジュール返却すれば中間モジュールを作成しなくても Dynami import が可能でした。

Dynamic import したいコンポーネント

// ManyComponents.js
export const MyComponent = /* ... */;
export const MyUnusedComponent = /* ... */;

React.lazy で Dynamic import

import { lazy } from 'react';
const MyComponent = lazy(() => 
  import("./MyComponent").then(({ MyComponent }) => ({
    default: MyComponent,
  }))
);

問題としては複数のモジュールをexport しているファイルだと一度全体を import してしまうので Tree shaking が効かなくなることかと思います。単体のモジュールしか export していない場合であれば中間モジュールのファイルを作成せず React.lazy で Dynamic import する方法として使えそうです。


[参考]

基礎から学ぶ React/React Hooks

基礎から学ぶ React/React Hooks

Amazon