Dynamic Components — ComponentRef, ViewContainerRef and CDK Portal

Dynamic components are created and destroyed programmatically at runtime rather than declared statically in templates. Use cases include toast notification systems (dynamically create and remove notification components), modal systems (programmatically create dialog content), and plugin architectures (load components based on configuration). Angular provides ViewContainerRef.createComponent() for programmatic component creation and Angular CDK’s Portal system for rendering components in overlay containers.

Dynamic Component Creation

import { Component, ViewContainerRef, ComponentRef,
         ViewChild, inject, signal, Type } from '@angular/core';

// ── Dynamic toast notifications ────────────────────────────────────────────
@Component({
  selector:   'app-toast-notification',
  standalone:  true,
  template: `
    <div class="toast" [class]="'toast-' + type">
      <mat-icon>{{ icon }}</mat-icon>
      <span>{{ message }}</span>
      <button mat-icon-button (click)="dismiss()">
        <mat-icon>close</mat-icon>
      </button>
    </div>
  `,
})
export class ToastNotificationComponent {
  message = '';
  type:    'success' | 'error' | 'info' = 'info';
  icon     = 'info';
  dismiss  = () => {};   // set by the creating service
}

// ── Toast service — dynamically creates notification components ───────────
@Injectable({ providedIn: 'root' })
export class ToastService {
  private container?: ViewContainerRef;
  private toasts: ComponentRef<ToastNotificationComponent>[] = [];

  // Must call this once in AppComponent
  setContainer(container: ViewContainerRef): void {
    this.container = container;
  }

  show(message: string, type: 'success' | 'error' | 'info' = 'info',
       duration = 4000): void {
    if (!this.container) return;

    const ref = this.container.createComponent(ToastNotificationComponent);

    // Set @Input values on the dynamic component
    ref.setInput('message', message);   // Angular 14+: setInput() for inputs
    ref.instance.type    = type;
    ref.instance.icon    = type === 'success' ? 'check_circle'
                         : type === 'error'   ? 'error'
                         : 'info';
    ref.instance.dismiss = () => this.dismiss(ref);

    this.toasts.push(ref);
    ref.changeDetectorRef.detectChanges();  // trigger initial render

    setTimeout(() => this.dismiss(ref), duration);
  }

  private dismiss(ref: ComponentRef<ToastNotificationComponent>): void {
    const idx = this.toasts.indexOf(ref);
    if (idx >= 0) {
      ref.destroy();              // destroys the component and removes from DOM
      this.toasts.splice(idx, 1);
    }
  }
}

// ── AppComponent — provides the container ─────────────────────────────────
@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    <ng-container #toastContainer />  <!-- mount point for dynamic toasts ── -->
    <router-outlet />
  `,
})
export class AppComponent implements AfterViewInit {
  @ViewChild('toastContainer', { read: ViewContainerRef })
  toastContainer!: ViewContainerRef;
  private toastService = inject(ToastService);

  ngAfterViewInit(): void {
    this.toastService.setContainer(this.toastContainer);
  }
}

// ── CDK Portal — for overlay-based dynamic components ─────────────────────
// import { ComponentPortal, PortalModule } from '@angular/cdk/portal';
// import { Overlay } from '@angular/cdk/overlay';
//
// const overlayRef = this.overlay.create({ positionStrategy: ... });
// const portal     = new ComponentPortal(MyDialogComponent);
// const ref        = overlayRef.attach(portal);
// ref.instance.title = 'My Dialog';
Note: ref.setInput('inputName', value) (Angular 14+) is the correct way to set @Input() properties on dynamically created components — it triggers Angular’s input transform pipeline and change detection. Directly setting ref.instance.inputProperty = value bypasses Angular’s input tracking and may not trigger ngOnChanges or signal-based input updates. Always use setInput() for @Input() properties and direct property assignment for regular (non-input) properties.
Tip: For most overlay/dialog/toast needs, Angular CDK’s Overlay API or MatDialog is simpler and more fully-featured than manual createComponent(). Reserve manual dynamic component creation for cases where CDK/Material does not fit — deeply custom rendering logic, plugin architectures where the component type is unknown at compile time, or performance-critical scenarios where you need direct control over component lifecycle. For standard notifications, MatSnackBar is the simpler choice.
Warning: Dynamic components created with ViewContainerRef.createComponent() must be destroyed manually with componentRef.destroy() when they are no longer needed. Unlike template-declared components (which Angular destroys when the parent is destroyed or the condition becomes false), dynamic components persist until explicitly destroyed. Failing to destroy them causes memory leaks and ghost components that respond to inputs and events but are visually hidden.

Common Mistakes

Mistake 1 — Not calling componentRef.destroy() on dismissal (memory leak)

❌ Wrong — removing the component from the DOM manually without calling destroy(); component stays alive in memory.

✅ Correct — always call ref.destroy() to clean up the component, its subscriptions, and its DOM nodes.

Mistake 2 — Setting @Input via instance property instead of setInput() (skips input pipeline)

❌ Wrong — ref.instance.title = 'Hello' for a signal-based input(); signal inputs require setInput().

✅ Correct — always use ref.setInput('title', 'Hello') for @Input() and input() properties.

🧠 Test Yourself

A dynamic toast component is created and its message @Input is set with ref.setInput('message', 'Hello'). Why is ref.changeDetectorRef.detectChanges() called after?