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.
Enable Lightning Web Security. To dynamically import and instantiate Lightning web components, you must enable Lightning Web Security.
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:
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>.
<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 component5 // to signal when it's attached to the DOM6 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.
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.
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.
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;45 connectedCallback(){6 import("lightning/concreteComponent")7 .then(({default: ctor})=>(this.componentConstructor = ctor))8 .catch((err)=> console.log("Error importing component"));9}1011 renderedCallback(){12 // this.refs.myCmp will be available on the next rendering cycle after the constructor is set13 if(this.refs.myCmp){14 // this.refs.myCmp will contain a reference to the DOM node15 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.
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.
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.js2import{LightningElement, api}from "lwc";34export default class extends LightningElement{5 @api text;6}
In the placeholder component, import your custom element.
1// myApp.js2import{LightningElement}from "lwc";3import DynamicCmp from "c/dynamicCmp";45export 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.
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>45<!-- Dynamically loads childA or childB -->6 <lwc:component lwc:is={dynamicCtor} lwc:spread={childProps} lwc:on={eventHandlers}>7 </lwc:component>89<!-- 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.js2import{LightningElement}from "lwc";3import ChildA from "c/childA";4import ChildB from "c/childB";56export default class DynamicComponent extends LightningElement{7 customEvent = "";8 dynamicCtor = ChildA; // Start with ChildA910 get childProps(){11 return this.dynamicCtor === ChildA12 ? {name: "Child A Name", age: 30}13 : {name: "Child B Name", age: 5};14}1516 get eventHandlers(){17 return this.dynamicCtor === ChildA18 ? {customEventA: this.handleCustomEventA}19 : {customEventB: this.handleCustomEventB};20}2122 handleCustomEventA(event){23 this.customEvent = `Hello from ${event.detail}`;24}2526 handleCustomEventB(event){27 this.customEvent = `Hello from ${event.detail}`;28}2930 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.
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.
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.
Dispatch the custom event in the child component, which is then handled by the parent component.
1import{LightningElement}from "lwc";23export 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";34export 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 analyzable2import("c/" + "analyzable"); // 👎: Not statically analyzable3import("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 THIS2// Example with non statically analyzable dynamic import:3import{LightningElement, api}from "lwc";45const KNOWN_TYPE = new Set(["bar", "pie", "line"]);67export default class App extends LightningElement{8 chartCtor;910 _type = "line";1112 @api13 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}2021 this._type = val;2223 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 THIS2// Example with static imports:3import{LightningElement, api}from "lwc";45import BarChart from "c/barChart";6import PieChart from "c/pieChart";7import LineChart from "c/lineChart";89const KNOWN_TYPE = new Set(["bar", "pie", "line"]);10const CHART_MAPPING = {11 bar: BarChart,12 pie: PieChart,13 line: LineChart,14};1516export default class App extends LightningElement{17 chartCtor;1819 _type = "line";2021 @api22 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}2930 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 THIS2// Example with statically analyzable dynamic import:3import{LightningElement, api}from "lwc";45const 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};1112export default class App extends LightningElement{13 chartCtor;1415 _type = "line";1617 @api18 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}2526 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 THIS2// Example with component name as prop:3import{LightningElement, api}from "lwc";45export default class Field extends LightningElement{6 rendererCtor;78 _renderer;910 @api11 get renderer(){12 return this._renderer;13}14 set renderer(val){15 this._renderer = val;1617 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 THIS2// Example with component constructor as prop:3import{LightningElement, api}from "lwc";45export 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.