| |

Angular 68 🅰️ Dynamic Components

Static templates work beautifully until the component you need to render isn’t known until runtime. A dashboard widget, a modal dialog configured by a service, or an ad banner that cycles through different layouts — these scenarios demand that Angular create components on the fly, after the application has already booted. Angular provides two primary tools for this: NgComponentOutlet for declarative template usage, and ViewContainerRef for imperative TypeScript control.

One important guideline before we begin: for lazy-loading use cases — delaying the download of a heavy component until it’s needed — Angular now recommends the built-in @defer feature instead of dynamic component loading. The @defer block automatically extracts component code into separate JavaScript chunks and loads them based on configured triggers like idle, viewport, interaction, hover, or timer. Dynamic component loading via NgComponentOutlet or ViewContainerRef is for when you genuinely need runtime flexibility in which component to render, not just when to load it.

Key point: NgComponentOutlet is a structural directive that instantiates a component and inserts its host view into the current view. ViewContainerRef is a node in Angular’s component tree that can contain content; any component or directive can inject it to get a reference to its location in the DOM. When you create a component with ViewContainerRef, Angular appends it as the next sibling of the component or directive that injected the container.


Why dynamic components exist

Static templates are the norm in Angular, and for good reason: they’re type-checked, tree-shakeable, and easy to reason about. But several real-world scenarios resist static rendering.

The runtime selection problem. An application that displays different components based on user role, feature flags, or configuration data cannot reference those components directly in a template. The template compiler needs to know at build time what components might appear. NgComponentOutlet solves this by accepting a component class as a binding — the template says “render whatever class this expression returns,” and Angular handles instantiation.

The imperative control problem. Sometimes you need to create components in response to events, not in response to template state. A button click that opens a modal, a service that spawns notification toasts, a drag-and-drop system that creates preview components — these are imperative actions. ViewContainerRef.createComponent() gives you a ComponentRef you can hold, inspect, and destroy at will.

The data binding problem. Dynamically created components still need inputs and outputs. Setting properties after creation is error-prone: you might miss a required input, forget to subscribe to an output, or set a value before the component initializes. Angular’s binding helpers — inputBinding, outputBinding, twoWayBinding — let you declare inputs and outputs at creation time, in a way that mirrors template syntax.

The lazy-loading problem. A settings page might have basic settings that load immediately and advanced settings that load only when the user clicks a button. JavaScript’s dynamic import() returns a module whose component class can be passed to NgComponentOutlet or ViewContainerRef. Angular’s @defer block wraps this pattern in a declarative syntax with built-in placeholder, loading, and error states.

The trade-off. Dynamic components add complexity. A static template is compiled ahead of time, type-checked, and optimized. A dynamic component bypasses some of those checks. Use dynamic rendering when the choice of component is genuinely runtime-dependent, not when a simple @if block would suffice.


a. NgComponentOutlet: Declarative Dynamic Rendering

NgComponentOutlet is a structural directive that instantiates a component and inserts its host view into the current view. It provides a declarative approach: you bind a component class to the directive, and Angular handles creation and insertion. This is the simplest way to render dynamic components when the decision logic lives in your template.

import { Component } from '@angular/core';
import { NgComponentOutlet } from '@angular/common';

@Component({
  selector: 'app-admin-bio',
  standalone: true,
  template: '<p>Admin Bio</p>',
})
export class AdminBioComponent {}

@Component({
  selector: 'app-standard-bio',
  standalone: true,
  template: '<p>Standard Bio</p>',
})
export class StandardBioComponent {}

@Component({
  selector: 'app-profile',
  standalone: true,
  imports: [NgComponentOutlet],
  template: `
    <p>Profile for {{ user().name }}</p>
    <ng-container *ngComponentOutlet="getBioComponent()" />
  `,
})
export class ProfileComponent {
  user = input.required<User>();

  getBioComponent() {
    return this.user().isAdmin ? AdminBioComponent : StandardBioComponent;
  }
}

The *ngComponentOutlet binding accepts the component class directly. When the value changes — say isAdmin flips — Angular detects the change and swaps the rendered component. The ng-container wrapper is idiomatic because the outlet directive is structural; it doesn’t create a DOM element of its own, so the projected component’s host element becomes the visible node.

NgComponentOutlet supports passing inputs to the dynamically rendered component through the inputs property. This property accepts an object where keys are input names and values are the input values. When the inputs object changes, Angular updates the dynamic component’s inputs and syncs it with the parent’s state.

@Component({
  selector: 'user-greeting',
  standalone: true,
  template: '<p>User: {{ username() }}</p><p>Role: {{ role() }}</p>',
})
export class UserGreetingComponent {
  username = input.required<string>();
  role = input<string>('guest');
}

@Component({
  selector: 'app-profile-view',
  standalone: true,
  imports: [NgComponentOutlet],
  template: `<ng-container *ngComponentOutlet="greetingComponent; inputs: greetingInputs()" />`,
})
export class ProfileViewComponent {
  greetingComponent = UserGreetingComponent;
  greetingInputs = signal({ username: 'ngAwesome', role: 'admin' });
}

The inputs property accepts a signal, so updating greetingInputs automatically flows the new values into the dynamic component. This is the modern, reactive way to pass data to dynamically rendered components.

NgComponentOutlet also exposes ngComponentOutletInjector for providing a custom injector, and ngComponentOutletContent for projection into the dynamic component’s <ng-content> slots. The content property is more complex — it expects an array of projection slots, where each element represents one <ng-content> in the dynamic component — and is rarely needed in practice.


b. ViewContainerRef: Imperative Control from TypeScript

When you need finer control — creating multiple components, inserting them at specific positions, or making decisions entirely in TypeScript — ViewContainerRef is the imperative alternative. Any component or directive can inject ViewContainerRef to get a reference to a view container at its location in the DOM.

import { Component, ViewContainerRef, inject } from '@angular/core';

@Component({
  selector: 'app-dynamic-host',
  standalone: true,
  template: `<button (click)="loadContent()">Load</button>`,
})
export class DynamicHostComponent {
  private vcr = inject(ViewContainerRef);

  loadContent() {
    const componentRef = this.vcr.createComponent(LeafContentComponent, {
      bindings: [
        inputBinding('label', () => 'Dynamic Content'),
      ],
    });
  }
}

@Component({
  selector: 'app-leaf-content',
  standalone: true,
  template: '<p>{{ label() }}</p>',
})
export class LeafContentComponent {
  label = input.required<string>();
}

The createComponent method creates the component and appends its host view as the next sibling of the component that injected the ViewContainerRef. In the DOM, the dynamically created component’s host element appears immediately after the host component’s own element. This placement is fixed — the component appears where the ViewContainerRef was injected, not where a template binding might suggest.

For older Angular versions, the pattern was different: you’d use ComponentFactoryResolver to resolve a factory and then call createComponent on the view container. That approach is now deprecated. Angular no longer requires component factories; the component class can be used directly since v13. The ComponentFactoryResolver and ComponentFactory APIs remain in the codebase for backward compatibility but should not be used in new code.

The createComponent method returns a ComponentRef — a reference to the created component instance. This reference gives you access to the component’s instance property (the actual class instance), its hostView (the change detector), and its destroy() method. Storing this reference lets you manipulate the component after creation: call methods on its instance, trigger change detection manually, or destroy it when no longer needed.

One important behavior: ViewContainerRef can hold multiple views. Calling createComponent repeatedly appends new components to the container. Calling clear() removes all views at once. Calling remove(index) removes a specific view. This makes the container a collection, not a single slot. For a modal host that should only ever have one modal open, check this.vcr.length before creating a new component.


c. Binding Inputs, Outputs, and Content

The binding helpers introduced alongside programmatic rendering solve a real problem: wiring up a dynamically created component used to require multiple lines of imperative code, and was easy to get wrong. The bindings array accepts three helper functions that mirror template syntax.

const componentRef = this.vcr.createComponent(ModalComponent, {
  bindings: [
    inputBinding('title', () => `Angular ${this.version()} is here!`),
    twoWayBinding('data', this.data),
    outputBinding('close', (result) => {
      console.log(result);
      this.vcr.clear();
    }),
  ],
  directives: [TitleColorDirective],
});

inputBinding('title', () => ...) sets an input whose value can be reactive — the arrow function is evaluated in the component’s injection context, so signals work naturally. When the signal read inside the arrow function changes, Angular updates the dynamic component’s input. twoWayBinding wires up a model input for automatic synchronization in both directions. outputBinding subscribes to an output event; the callback runs whenever the dynamic component emits.

The directives array applies host directives to the dynamically created component, extending its behavior without modifying its class. This is useful for cross-cutting concerns like logging, animation, or styling that you want to apply selectively to dynamic components.

For NgComponentOutlet, the same capability is expressed through the inputs parameter and the ngComponentOutletContent parameter for projection. The ngComponentOutletInjector parameter allows providing a custom injector when the dynamic component needs specific services or tokens. The injector parameter is particularly useful when the dynamic component needs access to route params, a specific service instance, or configuration that isn’t available from the parent’s injector.

There is a hydration caveat worth noting. ngComponentOutletContent does not support DOM nodes created with native DOM APIs — this causes NG0503 errors during hydration. If you need to project content into a dynamic component in a server-side rendered application, use Angular APIs to create the projected content or add ngSkipHydration to the component.


Complete Example Session

This session builds an ad banner that cycles through different ad components, demonstrating the full workflow.

// ============================================
// PART 1: THE AD COMPONENTS
// ============================================

import { Component, input, output } from '@angular/core';

@Component({
  selector: 'app-hero-job-ad',
  standalone: true,
  template: `
    <div class="job-ad">
      <h4>{{ headline() }}</h4>
      <p>{{ body() }}</p>
    </div>
  `,
})
export class HeroJobAdComponent {
  headline = input.required<string>();
  body = input.required<string>();
}

@Component({
  selector: 'app-hero-profile',
  standalone: true,
  template: `
    <div class="hero-profile">
      <h3>{{ name() }}</h3>
      <p>{{ bio() }}</p>
    </div>
  `,
})
export class HeroProfileComponent {
  name = input.required<string>();
  bio = input.required<string>();
}

// ============================================
// PART 2: THE AD SERVICE
// ============================================

import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class AdService {
  getAds() {
    return [
      {
        component: HeroJobAdComponent,
        inputs: { headline: 'Hiring Angular Devs', body: 'Join our team.' },
      },
      {
        component: HeroProfileComponent,
        inputs: { name: 'Spider-Man', bio: 'Friendly neighborhood hero.' },
      },
    ];
  }
}

// ============================================
// PART 3: THE AD BANNER WITH NGCOMPONENTOUTLET
// ============================================

import { Component, inject, signal } from '@angular/core';
import { NgComponentOutlet } from '@angular/common';
import { AdService } from './ad.service';

@Component({
  selector: 'app-ad-banner',
  standalone: true,
  imports: [NgComponentOutlet],
  template: `
    <div class="ad-banner">
      <ng-container *ngComponentOutlet="currentAd().component; inputs: currentAd().inputs" />
      <button (click)="displayNextAd()">Next</button>
    </div>
  `,
})
export class AdBannerComponent {
  private adList = inject(AdService).getAds();
  private currentAdIndex = signal(0);

  currentAd = computed(() => this.adList[this.currentAdIndex()]);

  displayNextAd() {
    this.currentAdIndex.update((i) => (i + 1) % this.adList.length);
  }
}

// ============================================
// PART 4: THE MODAL HOST WITH VIEWCONTAINERREF
// ============================================

import { Component, ViewContainerRef, inject, viewChild, signal } from '@angular/core';
import { inputBinding, twoWayBinding, outputBinding } from '@angular/core';

@Component({
  selector: 'app-modal-host',
  standalone: true,
  template: `
    <button (click)="openModal()">Open modal</button>
    <ng-container #modalContainer />
  `,
})
export class ModalHostComponent {
  private vcr = inject(ViewContainerRef);
  modalContainer = viewChild.required('modalContainer', { read: ViewContainerRef });
  title = signal('Hello');
  data = signal('');

  openModal() {
    if (!this.modalContainer().length) {
      this.modalContainer().createComponent(ModalComponent, {
        bindings: [
          inputBinding('title', () => `Angular ${this.title()} is here!`),
          twoWayBinding('data', this.data),
          outputBinding('close', (result) => {
            if (result) this.modalContainer().clear();
          }),
        ],
      });
    }
  }
}

// ============================================
// PART 5: THE VIEWCONTAINERREF CLEANUP
// ============================================

@Component({
  selector: 'app-toast-host',
  standalone: true,
  template: `<ng-container #toastContainer />`,
})
export class ToastHostComponent {
  private vcr = inject(ViewContainerRef);
  toastContainer = viewChild.required('toastContainer', { read: ViewContainerRef });

  showToast(message: string) {
    const ref = this.toastContainer().createComponent(ToastComponent, {
      bindings: [
        inputBinding('message', () => message),
        outputBinding('dismiss', () => {
          ref.destroy();
        }),
      ],
    });
  }
}

// ============================================
// PART 6: THE DYNAMIC IMPORT
// ============================================

@Component({
  selector: 'app-admin-settings',
  standalone: true,
  imports: [NgComponentOutlet],
  template: `
    <section>
      <h2>Basic settings</h2>
      <basic-settings />
    </section>
    <section>
      <h2>Advanced settings</h2>
      @if (!advancedSettings) {
        <button (click)="loadAdvanced()">Load advanced settings</button>
      }
      <ng-container *ngComponentOutlet="advancedSettings" />
    </section>
  `,
})
export class AdminSettingsComponent {
  advancedSettings: { new (): AdvancedSettingsComponent } | undefined;

  async loadAdvanced() {
    const { AdvancedSettingsComponent } = await import('./advanced-settings.component');
    this.advancedSettings = AdvancedSettingsComponent;
  }
}

// ============================================
// PART 7: THE NGCOMPONENTOUTLET INPUTS SIGNAL
// ============================================

@Component({
  selector: 'app-greeting-view',
  standalone: true,
  imports: [NgComponentOutlet],
  template: `<ng-container *ngComponentOutlet="greetingComponent; inputs: greetingInputs()" />`,
})
export class GreetingViewComponent {
  greetingComponent = UserGreetingComponent;
  greetingInputs = signal({ username: 'ngAwesome', role: 'admin' });
}

// ============================================
// PART 8: THE @DEFER ALTERNATIVE
// ============================================

@Component({
  selector: 'app-settings',
  standalone: true,
  template: `
    @defer (on interaction) {
      <advanced-settings />
    } @placeholder {
      <p>Click to load advanced settings</p>
    } @loading (minimum 500ms) {
      <img alt="loading..." src="loading.gif" />
    } @error {
      <p>Failed to load large component.</p>
    }
  `,
})
export class SettingsComponent {}

// ============================================
// PART 9: THE COMPONENTREF DESTROY
// ============================================

@Component({
  selector: 'app-destroy-demo',
  standalone: true,
  template: `<ng-container #container />`,
})
export class DestroyDemoComponent {
  private vcr = inject(ViewContainerRef);
  container = viewChild.required('container', { read: ViewContainerRef });
  private ref: ComponentRef<ToastComponent> | null = null;

  createAndDestroy() {
    this.ref = this.container().createComponent(ToastComponent);
    setTimeout(() => {
      this.ref?.destroy();
      this.ref = null;
    }, 3000);
  }
}

// ============================================
// PART 10: THE INJECTOR PARAMETER
// ============================================

import { Injector } from '@angular/core';

@Component({
  selector: 'app-injector-demo',
  standalone: true,
  template: `<ng-container *ngComponentOutlet="dynamicComponent; injector: customInjector" />`,
})
export class InjectorDemoComponent {
  dynamicComponent = ThemedComponent;
  customInjector = Injector.create({
    providers: [{ provide: THEME_TOKEN, useValue: 'dark' }],
    parent: inject(Injector),
  });
}

The ten parts cover the ad components, the ad service, the NgComponentOutlet ad banner, the ViewContainerRef modal host, the cleanup pattern, the dynamic import for lazy loading, the inputs signal for reactive binding, the @defer alternative, the ComponentRef.destroy() cleanup, and the custom injector parameter.


Quick Reference

The Two Approaches

ToolApproachUse When
NgComponentOutletDeclarative (template)Component choice is simple, inputs known
ViewContainerRef.createComponentImperative (TypeScript)Fine control, multiple instances, complex logic
@deferDeclarative blockLazy-loading heavy components, not dynamic choice

The Binding Helpers

HelperPurposeExample
inputBinding(name, fn)Set input valueinputBinding('title', () => 'Hello')
twoWayBinding(name, signal)Two-way bindingtwoWayBinding('data', this.data)
outputBinding(name, fn)Subscribe to outputoutputBinding('close', fn)

The NgComponentOutlet Parameters

ParameterPurpose
Component classThe component to render
inputsObject or signal of input values
injectorCustom injector for the component
contentProjected content slots (hydration caveat)

The ViewContainerRef Methods

MethodPurpose
createComponent()Create and insert a component
clear()Remove all views
remove(index)Remove a specific view
lengthNumber of views in container

The ComponentRef Properties

PropertyPurpose
instanceThe component class instance
hostViewThe change detector
destroy()Destroy the component

The @defer Triggers

TriggerDescription
idleBrowser is idle (default)
viewportContent enters viewport
interactionUser clicks or presses key
hoverMouse hovers over area
immediateAfter non-deferred content renders
timerAfter a specified duration

Best Practices

✅ Do This:

// Use NgComponentOutlet for simple template-driven cases
<ng-container *ngComponentOutlet="getBioComponent()" />  // ✅
// Use inputBinding/outputBinding helpers for clean setup
bindings: [
  inputBinding('title', () => 'Hello'),
  outputBinding('close', (result) => { ... }),
]                                                         // ✅
// Clean up dynamic components when no longer needed
ref.destroy();                                            // ✅
// Use @defer for lazy-loading use cases
@defer (on interaction) { <advanced-settings /> }         // ✅
// Check container length before creating
if (!this.vcr.length) { this.vcr.createComponent(Modal); } // ✅
// Use signals for reactive inputs with NgComponentOutlet
inputs: greetingInputs()                                  // ✅

❌ Don’t Do This:

// Don't manually set properties after createComponent
const ref = this.vcr.createComponent(ModalComponent);
ref.instance.title = 'Hello';  // ❌ use inputBinding
// Don't forget to destroy orphaned components
this.vcr.createComponent(ToastComponent);  // ❌ never destroyed
// Don't use dynamic loading just to delay download
// Use @defer instead.                                    // ❌
// Don't use ComponentFactoryResolver (deprecated)
const factory = this.resolver.resolveComponentFactory(Modal); // ❌
// Don't project native DOM nodes into dynamic components
// This causes NG0503 during hydration.                     // ❌

Common Pitfalls

PitfallWhy It HappensFix
Inputs not passed to dynamic componentMissing inputs or bindingsUse inputs for NgComponentOutlet, inputBinding for ViewContainerRef
Component not destroyedNo reference kept for cleanupStore ComponentRef and call destroy() or clear()
Output events not firingNo subscription wired upUse outputBinding or subscribe to componentRef.instance.event
Component appears in wrong placeViewContainerRef injected at wrong nodeInject at the host component or use viewChild with read
Hydration error NG0503Native DOM nodes in ngComponentOutletContentUse Angular APIs or ngSkipHydration
ComponentFactoryResolver usedOld pattern from pre-v13Use component class directly

Real-World Examples

1. Ad Banner with NgComponentOutlet

<ng-container *ngComponentOutlet="currentAd.component; inputs: currentAd.inputs" />

2. Modal with ViewContainerRef

this.vcr.createComponent(ModalComponent, {
  bindings: [outputBinding('close', () => this.vcr.clear())],
})

3. Toast with Auto-Destroy

const ref = this.toastContainer().createComponent(ToastComponent);
setTimeout(() => ref.destroy(), 3000);

4. Dynamic Import for Lazy Loading

const { AdvancedSettings } = await import('./advanced-settings.component');
this.advancedSettings = AdvancedSettings;

5. @defer with Placeholder

@defer (on interaction) { <large-cmp /> } @placeholder { <p>Click to load</p> }

6. Inputs Signal with NgComponentOutlet

<ng-container *ngComponentOutlet="comp; inputs: greetingInputs()" />

7. Custom Injector

Injector.create({ providers: [{ provide: THEME, useValue: 'dark' }], parent: inject(Injector) })

8. Check Container Before Creating

if (!this.vcr.length) { this.vcr.createComponent(Modal); }

9. Two-Way Binding with Dynamic Component

twoWayBinding('data', this.data)

10. ComponentRef Instance Access

const ref = this.vcr.createComponent(ModalComponent);
ref.instance.someMethod();

Visual: The Two Approaches

┌──────────────────────────────────────────────┐
│  NgComponentOutlet (DECLARATIVE)             │
│                                              │
│  <ng-container *ngComponentOutlet=           │
│    "getBioComponent()" />                    │
│                                              │
│  Template decides which component            │
│  Angular handles creation                    │
│  Simple, type-safe, no cleanup needed        │
│                                              │
├──────────────────────────────────────────────┤
│  ViewContainerRef (IMPERATIVE)               │
│                                              │
│  this.vcr.createComponent(ModalComponent, {  │
│    bindings: [inputBinding('title', ...)]    │
│  });                                         │
│                                              │
│  TypeScript decides when and how             │
│  Returns ComponentRef for control            │
│  Must manage cleanup                         │
│                                              │
└──────────────────────────────────────────────┘

Visual: Binding Helpers Flow

┌──────────────────────────────────────────────┐
│  createComponent(Modal, {                    │
│    bindings: [                               │
│      inputBinding('title', () => signal()),  │
│        │                                     │
│        └─> Modal.title() receives value      │
│                                              │
│      twoWayBinding('data', parentSignal),    │
│        │                                     │
│        └─> Syncs both directions             │
│                                              │
│      outputBinding('close', handler),        │
│        │                                     │
│        └─> handler runs on emit              │
│                                              │
│    ],                                        │
│    directives: [SomeDirective],              │
│      │                                       │
│      └─> Directive applied to Modal          │
│  })                                          │
│                                              │
└──────────────────────────────────────────────┘

Visual: ViewContainerRef DOM Placement

┌──────────────────────────────────────────────┐
│  BEFORE createComponent:                     │
│                                              │
│  <outer-container>                           │
│    <p>Start</p>                              │
│    <inner-item>                              │
│      <button>Load</button>  ← VCR injected   │
│    </inner-item>                             │
│    <p>End</p>                                │
│  </outer-container>                          │
│                                              │
│  AFTER createComponent(LeafContent):         │
│                                              │
│  <outer-container>                           │
│    <p>Start</p>                              │
│    <inner-item>                              │
│      <button>Load</button>                   │
│    </inner-item>                             │
│    <leaf-content>...</leaf-content>  ← HERE  │
│    <p>End</p>                                │
│  </outer-container>                          │
│                                              │
│  The component appears as the NEXT SIBLING   │
│  of the component that injected the VCR.     │
│                                              │
└──────────────────────────────────────────────┘

Visual: @defer Lifecycle

┌──────────────────────────────────────────────┐
│  @defer (on viewport) {                      │
│    <large-component />                       │
│  } @placeholder (minimum 500ms) {            │
│    <p>Placeholder content</p>                │
│  } @loading (after 100ms; minimum 1s) {      │
│    <img src="loading.gif" />                 │
│  } @error {                                  │
│    <p>Failed to load.</p>                    │
│  }                                           │
│                                              │
│  Flow:                                       │
│  1. Placeholder shows (min 500ms)            │
│  2. Trigger fires (viewport enters)          │
│  3. Loading shows (after 100ms, min 1s)      │
│  4. Component renders OR error shows         │
│                                              │
│  Dependencies must be standalone and         │
│  not referenced outside the @defer block.    │
│                                              │
└──────────────────────────────────────────────┘

Summary

ItemValue
Declarative toolNgComponentOutlet
Imperative toolViewContainerRef.createComponent()
Binding helpersinputBinding, outputBinding, twoWayBinding
Lazy-loading alternative@defer with triggers
DeprecatedComponentFactoryResolver (since v13)
CleanupComponentRef.destroy() or ViewContainerRef.clear()
Hydration caveatngComponentOutletContent with native DOM

Key takeaways:

  • NgComponentOutlet handles declarative dynamic rendering. Bind a component class to the directive, pass inputs via the inputs property, and Angular handles creation and insertion. It’s the simplest approach when the template can express which component to render.
  • ViewContainerRef.createComponent gives imperative control. Inject the container, call createComponent with the component class, and receive a ComponentRef for manipulation and cleanup. The component appears as the next sibling of the injecting node.
  • The binding helpers make wiring clean. inputBinding, outputBinding, and twoWayBinding declare inputs and outputs at creation time, mirroring template syntax and avoiding error-prone manual assignment.
  • @defer is the recommended lazy-loading tool. For delaying the download of heavy components, @defer extracts code into separate chunks and loads based on triggers. Use dynamic components for runtime choice, not just delayed timing.
  • Always manage cleanup. A dynamic component that isn’t destroyed leaks memory and change detection cycles. Store the ComponentRef and call destroy(), or use ViewContainerRef.clear() to remove all views.
  • ComponentFactoryResolver is deprecated. Since v13, you can pass component classes directly to createComponent or NgComponentOutlet. The old factory-based pattern should not appear in new code.
  • Hydration has caveats. ngComponentOutletContent does not support native DOM nodes; this causes NG0503 errors during hydration. Use Angular APIs or ngSkipHydration.

Remember: Dynamic components give Angular applications runtime flexibility that static templates cannot provide. NgComponentOutlet handles declarative scenarios where the template can express which component to render. ViewContainerRef.createComponent gives imperative control when logic belongs in TypeScript. The binding helpers make wiring as clean as template syntax. And for pure lazy-loading use cases, @defer is the recommended tool. Choose the right approach based on whether you need dynamic choice or dynamic timing.

Angular, dynamic components, ViewContainerRef, NgComponentOutlet


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!