Dynamically Instantiate Components

Dynamic component instantiation can help you to avoid loading large modules that you don’t always need. Also, you can instantiate a component instance when the underlying component constructor isn’t known until runtime. Dynamic import is a convenient solution to make a component more customizable. However, it isn’t always the best solution because of the runtime performance overhead it introduces, so don’t overuse it.

How to Work with Dynamic Lightning Web Components

Environment Setup and Caching 

Before developing with dynamic components, ensure that your org is configured to immediately reflect your code changes.

  1. Enable Lightning Web Security. To dynamically import and instantiate Lightning web components, you must enable Lightning Web Security.
  2. Disable persistent caching. To avoid issues where dynamic imports fail to update during development, navigate to Setup > Session Settings and uncheck Enable secure and persistent browser caching to improve performance. You should re-enable this feature in production for optimal performance.

Configure the Dynamic Component Capability 

To instantiate a dynamic component, a component’s configuration file must include the lightning__dynamicComponent capability. For example:

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>

To use this capability, you must set the apiVersion property to 55.0 or later.

For more information on a component’s configuration file, see Component Configuration File.

Dynamic Component Syntax 

To instantiate a component dynamically, use the <lwc:component> managed element with the lwc:is directive in a component’s HTML file. When building component names dynamically, make sure that you include the namespace. The syntax for the component name is c/componentName, where c is the default namespace.

Here’s an HTML template that uses <lwc:component>.

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

<lwc:component> serves as a placeholder in the DOM that renders the specified dynamic component. You must use <lwc:component> with the lwc:is directive.

The lwc:is directive provides an imported constructor at runtime to the <lwc:component> managed element. lwc:is accepts an expression that resolves to a LightningElement constructor at runtime.

If the constructor is falsy, the <lwc:component> tag along with all of its children aren’t rendered.

If the expression value is defined but not a LightningElement constructor, an error is thrown.

In the component’s JavaScript file, import the custom element using the import() dynamic import syntax.

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}

The import() call returns a promise that resolves to a LightningElement constructor. The element is then rendered instead of the lwc:component placeholder. The tag name used for the dynamic component is the value that’s returned for the given constructor.

Similar to a regular component, the dynamic component is instantiated and attached to the DOM. If the dynamic component’s constructor changes, the existing element is removed from the DOM.

In this example, the following HTML is rendered after the import completes.

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

Instead of using the then() method, you can use the operators async/await with try/catch to return the component constructor and handle network failures or invalid metadata. Because connectedCallback() is synchronous, call the async helper from connectedCallback() instead of marking the callback itself as async. For details, see Don’t mark lifecycle hooks as async.

1import { LightningElement } from "lwc";
2export default class App extends LightningElement {
3  componentConstructor;
4  connectedCallback() {
5    this.loadComponent();
6  }
7  async loadComponent() {
8    try {
9      const { default: ctor } = await import("c/myComponent");
10      this.componentConstructor = ctor;
11    } catch (err) {
12      console.error("Error importing component", err);
13    }
14  }
15}

Select a Dynamic Component 

A custom element must be attached to the DOM before you can select it. To select a dynamic component, use the lwc:ref directive or use an attribute that’s assigned to the component, such as a class name.

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

To identify if a dynamic component is attached to the DOM:

  • Use connectedCallback in the dynamic component to signal when it’s attached to the DOM.
  • Use renderedCallback on the parent component to detect when the dynamic component has rendered to the 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}

In Jest tests, the component’s tag name for a dynamic component is an internal default value. To select the dynamic component in a Jest test, we recommend that you use a different selector by appending a custom data attribute.

1<lwc:component
2    lwc:is={componentConstructor}
3    contact={contact}
4    data-id-dynamic-cmp="c-dynamic-component"
5></lwc:component>

In the Jest test, select the component using this syntax.

1const dynamicCmpEl = element.shadowRoot.querySelector(
2  '[data-id-dynamic-cmp="c-dynamic-component"]',
3);

For more information, see Write Jest Tests for Lightning Web Components.

Assign Attributes and Template Directives 

All supported HTML attributes that can be applied to an HTMLElement can also be applied to lwc:component.

Some examples include:

Dynamic components behave like standard Lightning web components. lwc:component supports the directives for HTML elements, except for lwc:external.

Dynamic Component's Children Elements 

You can include child elements on the dynamic component. <lwc:component> first renders the dynamic component and then its children. Each time the dynamic component changes, the existing element is removed from the DOM along with all of its children. The new dynamic component is then rendered along with its children.

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

Pass Properties in Markup 

Passing a property to a dynamic component is similar to passing a property to a child component. In the dynamic component, annotate the property with @api and use it in the template.

1// dynamicCmp.js
2import { LightningElement, api } from "lwc";
3
4export default class extends LightningElement {
5  @api text;
6}

In the placeholder component, import your custom element.

1// myApp.js
2import { LightningElement } from "lwc";
3import DynamicCmp from "c/dynamicCmp";
4
5export default class extends LightningElement {
6  componentConstructor = DynamicCmp;
7}

Pass in the value for the text property.

1<!-- myApp.html -->
2<template>
3  <lwc:component lwc:is={componentConstructor} text="I love dynamic components!"></lwc:component>
4</template>

In certain cases, it might not be possible to set all the potential properties a dynamic component can accept via the standard markup syntax. For example, when the component to be instantiated isn’t known in advance, or the components to be instantiated accept a different set of public properties.

In those specific cases, the lwc:spread directive can be used to dynamically set to dynamic component properties at runtime. lwc:spread also enables elements to accept an object that’s bound as properties at runtime.

Make the properties public by annotating them with @api.

1// dynamicCmp.js
2import { LightningElement, api } from "lwc";
3
4export default class extends LightningElement {
5  @api city;
6  @api state;
7}

Use the properties in your template.

1<!-- dynamicCmp.html -->
2<template>
3  <p>{city}, {state}</p>
4</template>

Import your custom element as usual, and create a childProps object with the property name and values.

1// myApp.js
2import { LightningElement } from "lwc";
3
4export default class extends LightningElement {
5  componentConstructor;
6  childProps = { city: "San Francisco", state: "CA" };
7
8  connectedCallback() {
9    // import your custom element
10  }
11}

Use lwc:spread to pass in your properties, which then renders “San Francisco, CA” on the dynamic component.

1<!-- myApp.html-->
2<template>
3  <lwc:component lwc:is={componentConstructor} lwc:spread={childProps}></lwc:component>
4</template>

Attach Event Listeners and Pass Properties to Dynamic Components 

In most cases, you want to set the properties and attach the event listeners based on which component is dynamically loaded. By using lwc:component with lwc:is, you can load components dynamically. Additionally, use the lwc:spread directive to set which properties to pass. Use lwc:on to dynamically set which event listeners to attach to the components.

The example shows how you can use all four directives to load components dynamically while passing properties to them and attaching event handlers for them.

The example includes 3 components

  • dynamic: Loads either childA or childB dynamically
  • childA : Dispatches the customEventA event with data
  • childB : Dispatches the customEventB event with data

The dynamic component defines the properties and an event handler, passing it to the childA and childB components.

1<!-- dynamic -->
2<template>
3  <lightning-button onclick={switchComponent} label="Switch Component"> </lightning-button>
4
5  <!-- Dynamically loads childA or childB -->
6  <lwc:component lwc:is={dynamicCtor} lwc:spread={childProps} lwc:on={eventHandlers}>
7  </lwc:component>
8
9  <!-- Displays the event detail from childA or childB -->
10  <p>Custom event received: {customEvent}</p>
11</template>

When the button on dynamic is clicked, it creates either the childA or childB component dynamically with the passed in properties and event handler.

1// dynamic.js
2import { LightningElement } from "lwc";
3import ChildA from "c/childA";
4import ChildB from "c/childB";
5
6export default class DynamicComponent extends LightningElement {
7  customEvent = "";
8  dynamicCtor = ChildA; // Start with ChildA
9
10  get childProps() {
11    return this.dynamicCtor === ChildA
12      ? { name: "Child A Name", age: 30 }
13      : { name: "Child B Name", age: 5 };
14  }
15
16  get eventHandlers() {
17    return this.dynamicCtor === ChildA
18      ? { customEventA: this.handleCustomEventA }
19      : { customEventB: this.handleCustomEventB };
20  }
21
22  handleCustomEventA(event) {
23    this.customEvent = `Hello from ${event.detail}`;
24  }
25
26  handleCustomEventB(event) {
27    this.customEvent = `Hello from ${event.detail}`;
28  }
29
30  switchComponent() {
31    this.dynamicCtor = this.dynamicCtor === ChildA ? ChildB : ChildA;
32  }
33}

childA and childB are similar components that differ only for the custom events they dispatch.

1<!-- childA -->
2<template>
3  <div>Child A Component</div>
4  <p>Name: {name}</p>
5  <p>Age: {age}</p>
6</template>

When you click the Switch Component button, the custom event on childA or childB is dispatched, depending on which child component is loaded.

1import { LightningElement, api } from "lwc";
2
3export default class ChildA extends LightningElement {
4  @api name;
5  @api age;
6
7  connectedCallback() {
8    this.dispatchEvent(new CustomEvent("customEventA", { detail: "Child A" }));
9  }
10}

Pass Record ID to a Dynamic Component 

A dynamic component can load data based on record context. To retrieve the record ID on a page, pass the record ID value to the dynamic component. Passing the record ID to a dynamic component is similar to passing a property to a child component.

1<!-- myApp.html -->
2<template>
3    <lwc:component record-id={recordId} lwc:is={componentConstructor} > </lwc:component>
4</template>

Retrieve the component module at runtime.

1// myApp.js
2import { LightningElement, api } from "lwc";
3
4export default class extends LightningElement {
5  componentConstructor;
6  @api recordId;
7
8  connectedCallback() {
9    this.loadComponent();
10  }
11
12  async loadComponent() {
13    const { default: ctor } = await import("c/myComponent");
14    this.componentConstructor = ctor;
15  }
16}

The record ID is passed to the dynamic component and you can use it to retrieve record data based on the ID.

Attach Event Listeners with Dynamic Component Loading 

To dynamically load components and attach event listeners to them, use lwc:on with lwc:component. The lwc:component element works together with the lwc:is directive to load a child component dynamically.

1<template>
2  <lwc:component lwc:is={childComponent} lwc:on={eventHandlers}></lwc:component>
3</template>

Define your event handlers for the child component.

1import { LightningElement } from "lwc";
2import ChildComponent from "c/childComponent";
3
4export default class ParentComponent extends LightningElement {
5  childComponent = ChildComponent;
6
7  eventHandlers = {
8    customEvent: this.handleCustomEvent,
9  };
10
11  handleCustomEvent(event) {
12    console.log("Custom event received:", event.detail);
13  }
14}

The child component contains a button that dispatches the custom event when clicked.

1<template>
2  <lightning-button onclick={dispatchCustomEvent} label="Click Me"> </lightning-button>
3</template>

Dispatch the custom event in the child component, which is then handled by the parent component.

1import { LightningElement } from "lwc";
2
3export default class ChildComponent extends LightningElement {
4  dispatchCustomEvent() {
5    const event = new CustomEvent("customEvent", {
6      detail: { message: "Hello from Child Component!" },
7    });
8    this.dispatchEvent(event);
9  }
10}

Dynamic Components in Packages 

You can use dynamic components in managed packages only. Dynamic components in unlocked packages aren’t supported.

Performance Considerations 

Since dynamic imports are “dynamic” by nature, the framework doesn’t prefetch those modules in advance, which can sometimes be detrimental to user experience.

When you use a static import statement, the framework delivers the component and all its dependencies in a single JavaScript file at runtime. To fetch a component that is dynamically imported, the framework has to do a network roundtrip if the content isn’t already stored in the browser HTTP cache.

In this example, the BundleExample component class is served with the StaticImport component class to the browser as a single JavaScript module. The DynamicImport component class is retrieved at runtime when the loadModule function is invoked.

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}

Consider these recommendations when working with dynamic components.

Make the dynamic imports statically analyzable 

While not currently implemented, future framework optimizations can optimize code where dynamic imports are statically analyzable. Pass a JavaScript string literal to the import() function:

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

There are always cases where dynamic imports can’t be statically analyzed. This scenario is especially true when the component name is defined via metadata. For all other cases, we strongly recommend making all your dynamic imports statically analyzable to avail of future framework optimizations.

Don’t overuse dynamic imports 

Dynamic import is a convenient solution to make a component more customizable. However, it isn’t always the best solution because of the runtime performance overhead it introduces. Let’s illustrate the potential pitfalls with some examples.

In this first example, we create a chart component, c/chart. It accepts chart type as a public property that can be set to either be bar, pie, or line. Internally, the component uses one of the c/barChart, c/pieChart, or c/lineChart components to render the chart based on the type.

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}

If the total bundle size of c/barChart, c/pieChart, and c/lineChart is small, it’s preferable to update the c/chart component to use static imports to avoid the network roundtrip at runtime. Generally, we recommend that you start with static import and use dynamic import if performance becomes an issue due to import of components that aren’t strictly needed.

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}

If statically importing all three modules negatively impacts runtime performance due to the bundle size increase, it’s still possible to update the example to turn the non-statically analyzable dynamic import into a statically analyzable one.

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}

Here’s another example illustrating this principle. In the example, we create a c/field component that is in charge of rendering an entity field value. Since it’s a generic component, it accepts a renderer public property that is the name of the component to use to render this field. Unlike in the previous example, the list of known renderers isn’t known in advance because the c/field component potentially accepts any component name.

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}

A more performant approach would be for the c/field component to accept the renderer constructor as a public property instead of the renderer component name. This only works if the component isn’t exposed to a builder, such as the Lightning App Builder.

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}

In this alternative design, the field component delegates to its parent the loading of the renderer component class. The parent component can now use either a static or a dynamic import depending on its requirements.

Use String Interpolation 

If you must drive the UI from Custom Metadata or Apex where the component name is only known at runtime, use string interpolation with backticks. Note that you must explicitly include the namespace (for example, c/) within the import string.

Constructing the import path from a metadata variable
1import(`c/${this.metaDataValue}`)

Interpolated strings can’t be evaluated during the build process, which prevents the framework from pre-bundling your dependencies. This adds unnecessary runtime overhead. Stick to static mapping unless you absolutely need dynamic values.

Tooling and Debugging 

To confirm that a dynamic component has been swapped (and not just hidden), use your browser developer tools to inspect the DOM. The custom element tag should be removed and replaced when the componentConstructor changes.

See Also