SfSkills Domains Docs GitHub

Home / lwc / lwc-dynamic-components

lwc-dynamic-components

Dynamic LWC component creation using the `lwc:component` directive, lazy-loaded dynamic imports (`import()`), and runtime component resolution for conditional rendering at scale. Triggers: 'render different components based on record type', 'dynamically load lwc at runtime', 'lwc:component lwc:is constructor', 'lazy load component only when needed', 'dynamic import lwc'. NOT for lwc:if over a fixed component set — use lwc/lwc-conditional-rendering. NOT for page performance tuning — use lwc/lwc-performance.

Open the skill folder on GitHub


LWC Dynamic Components

Use this skill when a Lightning Web Component must select and render a different child component at runtime — not at build time — based on record type, user role, server-side configuration, or a feature flag that cannot be resolved statically. The lwc:component directive combined with dynamic import() is the platform-supported mechanism for this pattern since Spring '23.


Before Starting

Gather this context before working on anything in this domain:


Core Concepts

The lwc:component Directive and lwc:is

The lwc:component tag is a special host element introduced in Spring '23 that renders whatever constructor is assigned to its lwc:is property. The template syntax is:

<lwc:component lwc:is={dynamicCtor}></lwc:component>

lwc:is must receive a component constructor (a class reference), not a string name. Passing a string does nothing and renders no component. When lwc:is is null or undefined, nothing is rendered, which is the correct initial state before the import resolves.

Dynamic Import and the default Export

The import() function loads a module asynchronously and returns a Promise that resolves to the module object. LWC component modules export their class as the default export, so you must destructure default to get the constructor:

// Correct
const { default: Ctor } = await import('c/myComponent');
this.dynamicCtor = Ctor;

// Also correct (explicit)
const module = await import('c/myComponent');
this.dynamicCtor = module.default;

// Wrong — this sets lwc:is to the module object, not the constructor
this.dynamicCtor = await import('c/myComponent');

Assign the constructor to a reactive property decorated with @track or declared as a class field so the template re-evaluates when it changes.

Lazy Loading and Bundle Splitting

Dynamic imports cause the module to be loaded as a separate chunk at runtime rather than included in the page's initial JavaScript bundle. This is the main performance benefit: a large component that is only sometimes needed does not inflate the first-paint payload. The component is fetched the first time import() is called; subsequent imports of the same module resolve from cache.

This also means there is a network round-trip the first time the component loads. For components that are almost always needed, a static import is cheaper. Reserve dynamic import for components that are genuinely conditional and large enough to justify the separate fetch.

Property Passing and Event Handling

After setting lwc:is, you can pass @api properties to the dynamic component using standard attribute binding on the lwc:component tag. The dynamic child can dispatch custom events; the parent listens with a standard oneventname handler on the lwc:component element. There is no difference from static composition once the constructor is resolved.


Common Patterns

Mode 1: Implement a Dynamic Component Renderer

When to use: You need to select from a known set of LWC components at runtime based on record type, user role, or server-returned configuration.

How it works:

  1. Declare a reactive property for the constructor and an optional map for @api inputs:
import { LightningElement, api, track } from 'lwc';

export default class DynamicFormRouter extends LightningElement {
    @api recordTypeId;
    @track dynamicCtor = null;
    @track componentProps = {};

    async connectedCallback() {
        await this._loadComponent();
    }

    async _loadComponent() {
        try {
            let mod;
            if (this.recordTypeId === '012ABC') {
                mod = await import('c/caseDetailView');
            } else if (this.recordTypeId === '012DEF') {
                mod = await import('c/orderDetailView');
            } else {
                mod = await import('c/genericDetailView');
            }
            this.dynamicCtor = mod.default;
            this.componentProps = { recordId: this.recordTypeId };
        } catch (err) {
            console.error('Failed to load dynamic component', err);
            this.dynamicCtor = null;
        }
    }
}
  1. In the template, render the dynamic component and bind properties:
<template>
    <template lwc:if={dynamicCtor}>
        <lwc:component lwc:is={dynamicCtor}
                       record-id={componentProps.recordId}
                       onstatuschange={handleStatusChange}>
        </lwc:component>
    </template>
    <template lwc:else>
        <lightning-spinner alternative-text="Loading..."></lightning-spinner>
    </template>
</template>

Why not the alternative: Using a chain of lwc:if blocks with statically imported components works for a small fixed set, but it instantiates all imported modules in the bundle even when only one renders. Dynamic import defers the unused ones entirely.

Mode 2: Review an Existing Dynamic Component Implementation

Check for:

Mode 3: Troubleshoot a Broken Dynamic Component

Common failure modes and their root cause:

SymptomLikely Cause
Nothing renders, no errorlwc:is is receiving the module object instead of module.default
lwc:is binding ignoredUsing this in an unlocked package; dynamic import is not supported
TypeError on importLWS is off; the dynamic import API is not available
Blank area with no errortry/catch swallowed the error; check the console
Props not reaching childProperties not bound on <lwc:component> or @api missing in child
Import specifier error at buildModule specifier is constructed from a variable; must be a literal string

Decision Guidance

SituationRecommended ApproachReason
Small fixed set of components, always known at build timelwc:if / lwc:elseif with static importsSimpler, no runtime overhead, works in unlocked packages
Large or rarely needed component resolved from metadata at runtimelwc:component with dynamic importDefers bundle cost; component only loaded when needed
Component chosen from a configuration table in a managed packageDynamic import with a switch/mapClean separation of routing logic from rendering
Unlocked package deploymentStatic conditional rendering onlyDynamic import is not supported in unlocked packages
Component must fire events back to parentStandard oneventname on <lwc:component>No special wiring needed; events work identically
Wire data needed before deciding which component to loadWire first, then await import() in a reactive handlerWire data is available in time; import is deferred until needed

Step-by-step instructions for an AI agent or practitioner activating this skill:

  1. Gather context — confirm the org edition, relevant objects, and current configuration state
  2. Review official sources — check the references in this skill's well-architected.md before making changes
  3. Implement or advise — apply the patterns from Core Concepts and Common Patterns sections above
  4. Validate — run the skill's checker script and verify against the Review Checklist below
  5. Document — record any deviations from standard patterns and update the template if needed

Review Checklist

Run through these before marking work in this area complete:


Salesforce-Specific Gotchas

Non-obvious platform behaviors that cause real production problems:

  1. module.default omission silently renders nothing — Assigning the Promise result of await import('c/foo') directly to lwc:is passes a module namespace object, not a constructor. The component silently renders nothing with no console error in most orgs. Always destructure default.
  2. Unlocked packages do not support dynamic import — The lwc:component dynamic loading pattern is supported only in managed packages. Teams building in unlocked packages who encounter this will see a runtime failure. There is no workaround short of switching package type.
  3. The import specifier must be a static string — The LWC build toolchain analyzes import calls at compile time to build the module chunk graph. A computed specifier like `c/${componentName}` will cause a build error or be silently ignored. All candidate components must appear as literal import strings in the source.
  4. Dynamic imports are not prefetched — Unlike static imports that are included in the page bundle, dynamic imports fetch their chunk on the first call. This introduces a round-trip latency the first time the user triggers the load. Show a loading indicator and consider whether the round-trip cost is acceptable.
  5. LWS must be enabled — Legacy Locker Service does not support import(). If an org has not migrated to Lightning Web Security, dynamic components will fail with a runtime error. Check Setup > Session Settings.

Output Artifacts

ArtifactDescription
Dynamic component routerJavaScript class with async connectedCallback or reactive handler wiring lwc:is from a module.default assignment
Template with lwc:componentHTML template binding lwc:is, @api props, and event handlers on the dynamic element
Error handling wrappertry/catch block with fallback state for failed module loads
Checker reportFile-level findings from scripts/check_lwc_dynamic.py for common misconfigurations

Package files