30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

React.Suspense と React.lazy でコンポーネントを動的に読み込む

React.SuspenseReact.lazyを使うことで、import()で動的に読み込んだコンポーネントを通常のコンポーネントとしてレンダリングすることができる。
動的読み込みはパフォーマンス向上のためなどに使われるが、それを簡単に React アプリに取り入れることができる。

import()の概要はこちらを参照。
numb86-tech.hatenablog.com

この記事に出てくるコードは React のv16.10.2で動作確認している。

React.lazy

React.lazyは、コンポーネントを返す関数。引数には、import()の返り値をそのまま返す関数を渡す。そしてimport()で読み込まれるモジュールは、コンポーネントをdefaultでエクスポートしている必要がある。
そのため、以下のようになる。

// One.js

import React from 'react';

const One = () => <span>1</span>;

export default One;

// App.js

const One = React.lazy(() => import('./One'));

これで、Oneを動的に読み込み、それでいて普通のコンポーネントと同じように使えるようになった。

React.Suspense

動的に読み込んだコンポーネントは、必ずSuspense要素でラップされる必要がある。そしてSuspense要素にはfallback属性が必須である。
具体的には以下のようになる。

<Suspense fallback="loading...">
  <One />
</Suspense>

Oneが読み込まれるまではfallbackに渡された内容が表示され、読み込みが終わるとOneの表示に切り替わる。
上記の例ではfallbackに文字列を渡しているが、React 要素なら何でもよい。

コード全体は以下のようになる。

// One.js

import React from 'react';

const One = () => <span>1</span>;

export default One;

// App.js

import React, {Suspense} from 'react';

const One = React.lazy(() => import('./One'));

const App = () => {
  return (
    <div>
      <Suspense fallback="loading...">
        <One />
      </Suspense>
    </div>
  );
};

export default App;

一瞬だけloading...が表示されたあと、1が表示される。

Suspenseには、動的に読み込んだコンポーネントを複数ラップできる。また、動的に読み込んだコンポーネント以外のコンポーネントも、ラップできる。

// Two.js

import React from 'react';

const Two = () => <span>2</span>;

export default Two;

// App.js

import React, {Suspense} from 'react';

const One = React.lazy(() => import('./One'));
const Two = React.lazy(() => import('./Two'));

const App = () => {
  return (
    <div>
      <Suspense fallback="loading...">
        <One />
        <Two />
        <span>3</span>
      </Suspense>
    </div>
  );
};

export default App;

動的に読み込んだコンポーネントを複数ラップした場合、全てのコンポーネントが読み込まれた時点で、ラップした内容を表示する。それまではfallbackの値が表示される。

それを確認するため、OneTwoを時間差で読み込ませる。

import React, {Suspense} from 'react';

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

const One = React.lazy(() => sleep(1000).then(() => import('./One')));
const Two = React.lazy(() => sleep(2000).then(() => import('./Two')));

const App = () => {
  return (
    <div>
      <Suspense fallback="loading...">
        <One />
        <Two />
        <span>3</span>
      </Suspense>
    </div>
  );
};

export default App;

このようにすると、約2秒間loading...が表示され、その後123が表示される。読み込みが終わったものから順次表示する、とはならない。

条件分岐で読み込むコンポーネントを変える

以下のコードでは、isOnetruthyのときにOneが、falsyのときにTwoが、それぞれ読み込まれる。
該当しないほうのコンポーネントは、表示に使う使わないではなく、そもそも読み込まれない。

import React, {Suspense} from 'react';

const isOne = true;

const TargetComponent = React.lazy(() => {
  if (isOne) return import('./One');
  return import('./Two');
});

const App = () => {
  return (
    <div>
      <Suspense fallback="loading...">
        <TargetComponent />
      </Suspense>
    </div>
  );
};

export default App;

参考資料

Dynamic import() で JavaScript ファイルを動的に読み込む

ECMAScript では、モジュールを読み込むための仕組みとしてimport文を定義している。

// sub.js

export const foo = 1;
export default 2;

// main.js

import value, {foo} from './sub.js';

console.log(value, foo); // 2 1

import文は仕様上、トップレベルで書くことになっており、以下のような書き方はシンタックスエラーになる。

// main.js

// SyntaxError: 'import' and 'export' may only appear at the top level
const call = () => {
  import value, {foo} from './sub.js';
};

// SyntaxError: 'import' and 'export' may only appear at the top level
if (true) {
  import value, {foo} from './sub.js';
}

そして、読み込まれるファイルは、読み込まれた瞬間に実行される。
そのため、以下のmain.jsを実行すると、submain2の順番に表示される。

// sub.js

console.log('sub');

export const foo = 1;
export default 2;

// main.js

import value from './sub.js';

console.log('main');
console.log(value);

ES2020 で追加される仕様のひとつであるimport()は、これまでのimport文と異なり、モジュールを動的に読み込む。
また、トップレベルでないと使えないという制約もない。

読み込みたいモジュールのパスを引数として渡すと、読み込んだモジュールをPromiseでラップして返す。

// sub.js

export const foo = 1;
export default 2;

// main.js

console.log(import('./sub.js') instanceof Promise); // true

import('./sub.js').then(res => {
  console.log(res.default); // 2
  console.log(res.foo); // 1
});

非同期処理なので、以下のmain.jsを実行すると先にmainが表示され、その後にsubが表示される。

// sub.js

console.log('sub');

export const foo = 1;
export default 2;

// main.js

import('./sub.js');

console.log('main');

関数のなかで使うこともできる。

// sub.js

export const foo = 1;
export default 2;

// main.js

const call = () => {
  return import('./sub.js').then(res => {
    return res.default;
  });
};

call().then(res => {
  console.log(res); // 2
});

フラグに応じて読み込むファイルを変える、ということも可能になる。
以下のコードでは、sub.jssomeFlagtruthyのときにのみ読み込まれる。

// sub.js

export default 'This is confidential information.';

// main.js

(async () => {
  const someFlag = true;

  const message = someFlag
    ? await import('./sub.js').then(res => res.default)
    : 'please login';

  console.log(message); // This is confidential information.
})();