コンポーネントの動的なインスタンス化

コンポーネントの動的なインスタンス化は、必ずしも必要ではない大きなモジュールの読み込みを回避するために役立ちます。また、基盤となるコンポーネントのコンストラクタが実行時まで知らされない場合でも、コンポーネントインスタンスをインスタンス化できます。動的インポートは、コンポーネントのカスタマイズの自由度を高める便利な解決方法です。ただし、実行時にパフォーマンスのオーバヘッドが発生するため、最良の解決方法ではない場合もあり、過度に使用しないように注意してください。

Lightning Web コンポーネントを動的にインポートおよびインスタンス化するには、Lightning Web セキュリティを有効化する必要があります。

Important

動的コンポーネントの機能の設定 

動的コンポーネントをインスタンス化するには、コンポーネントの設定ファイルに lightning__dynamicComponent 機能を含める必要がある。次に例を示します。

1<?xml version="1.0" encoding="UTF-8"?>
2<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
3  <apiVersion>59.0</apiVersion>
4  <capabilities>
5    <capability>lightning__dynamicComponent</capability>
6  </capabilities>
7</LightningComponentBundle>

この機能を使用するには、apiVersion プロパティを 55.0 以降に設定する必要があります。

コンポーネントの設定ファイルの詳細については、「コンポーネントの設定ファイル」を参照してください。

動的コンポーネントの構文 

コンポーネントを動的にインスタンス化するには、<lwc:component> 管理要素をコンポーネントの HTML ファイルで lwc:is ディレクティブと共に使用します。

次に、<lwc:component> を使用する HTML テンプレートを示します。

1<template>
2    <div class="container">
3        <lwc:component lwc:is={componentConstructor}></lwc:component>
4    </div>
5</template>

<lwc:component> は指定された動的コンポーネントを表示する DOM のプレースホルダとして機能します。<lwc:component> は、lwc:is ディレクティブと共に使用する必要があります。

lwc:is ディレクティブは、<lwc:component> 管理要素に、実行時にインポートされるコンストラクタを提供します。lwc:is は、実行時に LightningElement コンストラクタを解決する式を受け取ります。

コンストラクタが偽値 (falsy) の場合、<lwc:component> タグとその子要素は表示されません。

式の値が定義されているが、LightningElement コンストラクタではない場合、エラーがスローされます。

コンポーネントの JavaScript ファイルで、import() 動的インポート構文を使用してカスタム要素をインポートします。

1import { LightningElement } from "lwc";
2export default class extends LightningElement {
3  componentConstructor;
4  // Use connectedCallback() on the dynamic component
5  // to signal when it's attached to the DOM
6  connectedCallback() {
7    import("c/concreteComponent")
8      .then(({ default: ctor }) => (this.componentConstructor = ctor))
9      .catch((err) => console.log("Error importing component"));
10  }
11}

import() コールは、LightningElement コンストラクタを解決する Promise を返します。次にその要素が lwc:component プレースホルダの代わりに表示されます。動的コンポーネントに使用されるタグ名は、特定のコンストラクタで返される値です。

通常のコンポーネントと同様に、動的コンポーネントはインスタンス化され、DOM に添付されます。動的コンポーネントのコンストラクタが変更されると、既存の要素は DOM から削除されます。

次の例では、インポートが完了すると次のような HTML が表示されます。

1<div class="container">
2  <c-concrete-component></c-concrete-component>
3</div>

then() メソッドを使用する代わりに、asyncawait 演算子を使用してコンポーネントのコンストラクタを返すこともできます。

1import { LightningElement } from "lwc";
2export default class App extends LightningElement {
3  componentConstructor;
4  async connectedCallback() {
5    const { default: ctor } = await import("c/myComponent");
6    this.componentConstructor = ctor;
7  }
8}

動的コンポーネントの選択 

カスタム要素を選択するには、その要素が DOM にあらかじめ添付されている必要があります。動的コンポーネントを選択するには、lwc:ref ディレクティブを使用するか、クラス名などのコンポーネントに割り当てられた属性を使用します。

1<template>
2    <lwc:component lwc:is={componentConstructor}
3                lwc:ref="myCmp">
4    </lwc:component>
5</template>

動的コンポーネントが DOM に添付されているかどうかを確認する手順は、次のとおりです。

  • 動的コンポーネントで connectedCallback を使用して、DOM に添付されたときに通知します。
  • 親コンポーネントで renderedCallback を使用して、動的コンポーネントが DOM に添付されたことを検出します。
1import { LightningElement } from "lwc";
2export default class extends LightningElement {
3  componentConstructor;
4
5  connectedCallback() {
6    import("lightning/concreteComponent")
7      .then(({ default: ctor }) => (this.componentConstructor = ctor))
8      .catch((err) => console.log("Error importing component"));
9  }
10
11  renderedCallback() {
12    // this.refs.myCmp will be available on the next rendering cycle after the constructor is set
13    if (this.refs.myCmp) {
14      // this.refs.myCmp will contain a reference to the DOM node
15      console.log(this.refs.myCmp);
16    }
17  }
18}

属性の割り当て 

HTMLElement に適用可能なサポート対象の HTML 属性はすべて、lwc:component にも適用できます。

たとえば、次のような例が考えられます。

子要素 

動的コンポーネントには子要素を含めることができます。<lwc:component> は最初に動的コンポーネントをレンダリングし、次にその子コンポーネントをレンダリングします。動的コンポーネントが変更されるたびに、既存の要素はすべての子要素と共に DOM から削除されます。新しい動的コンポーネントが、その子コンポーネントと共に表示されます。

1<template>
2    <lwc:component lwc:is={ctor}>
3        <span>child</span>
4    </lwc:component>
5</template>

パッケージ内の動的コンポーネント 

動的コンポーネントは管理パッケージでのみ使用できます。ロック解除済みパッケージでは、動的コンポーネントはサポートされません。

パフォーマンス上の考慮事項 

動的インポートは本質的に「動的」であるため、フレームワークで、そのモジュールは事前に取得されません。この動作は、ユーザエクスペリエンスに悪影響を及ぼす可能性があります。

静的インポートステートメントを使用すると、フレームワークは実行時にコンポーネントとすべての連動関係を 1 つの JavaScript ファイルで配信します。動的にインポートされるコンポーネントを取得するときに、コンテンツがブラウザの HTTP キャッシュにまだ保存されていない場合、フレームワークはネットワークを往復する必要があります。

この例では、BundleExample コンポーネントクラスが StaticImport コンポーネントクラスと共に、1 つの JavaScript モジュールとしてブラウザに提供されます。DynamicImport コンポーネントクラスは、実行時、loadModule 関数が呼び出されたときに取得されます。

1import { LightningElement } from "lwc";
2import StaticImport from "c/static-import";
3
4export default class BundleExample extends LightningElement {
5  async loadModule() {
6    const { default: DynamicImport } = await import("c/dynamic-import");
7    return DynamicImport;
8  }
9}

動的コンポーネントを使用する場合は、次の推奨事項に留意してください。

動的インポートを静的に分析可能にする 

現時点では実装されていませんが、将来のフレームワークの最適化では、動的インポートが静的に分析可能な場合、コードを最適化できる可能性があります。次のように JavaScript 文字列リテラルを import() 関数に渡します。

1import("c/analyzable"); // 👍: Statically analyzable
2import("c/" + "analyzable"); // 👎: Not statically analyzable
3import("c/" + componentName); // 👎: Not statically analyzable

動的インポートを静的に分析できない状況は、常にあり得ます。特に、コンポーネント名がメタデータを介して定義されている場合が、これに当てはまります。それ以外の場合は、将来的なフレームワークの最適化を利用できるように、動的インポートはすべて静的に解析できるようにすることを強くお勧めします。

動的インポートを過度に使用しない 

動的インポートは、コンポーネントのカスタマイズの自由度を高める便利な解決方法です。ただし、実行時にパフォーマンスのオーバヘッドが発生するため、最良の解決方法ではない場合もあります。この場合の注意が必要な状況についていくつかの例を挙げて説明します。

この最初の例では、グラフコンポーネント c/chart を作成します。グラフの種類を公開プロパティとして受け取り、棒グラフ、円グラフ、折れ線グラフのいずれかに設定できます。このコンポーネントは内部的に、c/barChart、c/pieChart、または c/lineChart コンポーネントのいずれかを使用して、種類に基づいてグラフを表示します。

1// 👎 DON'T USE THIS
2// Example with non statically analyzable dynamic import:
3import { LightningElement, api } from "lwc";
4
5const KNOWN_TYPE = new Set(["bar", "pie", "line"]);
6
7export default class App extends LightningElement {
8  chartCtor;
9
10  _type = "line";
11
12  @api
13  get type() {
14    return this._type;
15  }
16  set type(val) {
17    if (!KNOWN_TYPE.has(val)) {
18      console.warn(`Unknown chart type: ${val}`);
19    }
20
21    this._type = val;
22
23    const chartComponentName = `c/${val}Chart`;
24    import(chartComponentName).then(({ default: ChartCtor }) => {
25      this.chartCtor = ChartCtor;
26    });
27  }
28}

c/barChartc/pieChartc/lineChart のバンドルサイズの合計が小さい場合は、静的インポートを使用して実行時のネットワークの往復処理を避けるように c/chart コンポーネントを更新することをお勧めします。通常は、静的インポートから始めることをお勧めします。そして厳密に必要ではないコンポーネントのインポートによってパフォーマンスの問題が生じる場合には、動的インポートを使用してください。

1// 👍 USE THIS
2// Example with static imports:
3import { LightningElement, api } from "lwc";
4
5import BarChart from "c/barChart";
6import PieChart from "c/pieChart";
7import LineChart from "c/lineChart";
8
9const KNOWN_TYPE = new Set(["bar", "pie", "line"]);
10const CHART_MAPPING = {
11  bar: BarChart,
12  pie: PieChart,
13  line: LineChart,
14};
15
16export default class App extends LightningElement {
17  chartCtor;
18
19  _type = "line";
20
21  @api
22  get type() {
23    return this._type;
24  }
25  set type(val) {
26    if (!KNOWN_TYPE.has(val)) {
27      console.warn(`Unknown chart type: ${val}`);
28    }
29
30    this._type = val;
31    this.chartCtor = CHART_MAPPING[val];
32  }
33}

3 つのモジュールすべてを静的にインポートするとバンドルサイズが大きくなり実行時のパフォーマンスに悪影響が出る場合でも、静的に解析できない動的インポートを静的に解析可能なインポートに変更するように、サンプルを更新することは可能です。

1// 👍 USE THIS
2// Example with statically analyzable dynamic import:
3import { LightningElement, api } from "lwc";
4
5const KNOWN_TYPE = new Set(["bar", "pie", "line"]);
6const CHART_MAPPING = {
7  bar: () => import("c/barChart"),
8  pie: () => import("c/pieChart"),
9  line: () => import("c/lineChart"),
10};
11
12export default class App extends LightningElement {
13  chartCtor;
14
15  _type = "line";
16
17  @api
18  get type() {
19    return this._type;
20  }
21  set type(val) {
22    if (!KNOWN_TYPE.has(val)) {
23      console.warn(`Unknown chart type: ${val}`);
24    }
25
26    this._type = val;
27    CHART_MAPPING[val]().then(({ default: ChartCtor }) => {
28      this.chartCtor = ChartCtor;
29    });
30  }
31}

この考え方を説明する別の例を次に示します。この例では、エンティティ項目の値を表示する c/field コンポーネントを作成します。これは標準コンポーネントなので、この項目の表示に使用するコンポーネントの名前であるレンダラーの公開プロパティを受け取ります。前の例とは異なり、c/field コンポーネントは任意のコンポーネント名を受け入れる可能性があるため、既知のレンダラーのリストは事前には知らされません。

1// 👎 DON'T USE THIS
2// Example with component name as prop:
3import { LightningElement, api } from "lwc";
4
5export default class Field extends LightningElement {
6  rendererCtor;
7
8  _renderer;
9
10  @api
11  get renderer() {
12    return this._renderer;
13  }
14  set renderer(val) {
15    this._renderer = val;
16
17    import(val).then(({ default: rendererCtor }) => {
18      this.rendererCtor = rendererCtor;
19    });
20  }
21}

よりパフォーマンスが向上する方法として、c/field コンポーネントが、レンダラーのコンストラクタをコンポーネント名ではなく、公開プロパティとして受け取る方法があります。これは、コンポーネントが Lightning アプリケーションビルダーなどのビルダーに公開されていない場合にのみ機能します。

1// 👍 USE THIS
2// Example with component constructor as prop:
3import { LightningElement, api } from "lwc";
4
5export default class Field extends LightningElement {
6  @api rendererCtor;
7}

この代替設計では、項目コンポーネントはレンダラーのコンポーネントクラスの読み込みを親に委任します。これにより親コンポーネントは、その要件に応じて静的インポートまたは動的インポートのいずれかを使用できるようになります。

関連トピック

The Japanese Summer '24 guide is now live

日本語の Summer '24 ガイドが公開されました! 「Component Reference (コンポーネントリファレンス)」は、以前と同様にコンポーネントライブラリにあります。