skip to content

For an Angular component created from code, why use ComponentRef.setInput or inputBinding instead of assigning ref.instance fields, and how do outputBinding and twoWayBinding fit in?

level: middleimportance: should knowfreq 40%

answer

  1. public names, dirty marking
  2. a signal input is read-only
  3. declare bindings at creation
  4. outputs cleaned up with the component
  5. value plus <name>Change

basics

~20 s

Assigning ref.instance fields skips ngOnChanges and OnPush dirty marking and cannot set signal inputs. setInput sets an input by public name and marks the view dirty; inputBinding, outputBinding and twoWayBinding declare reactive template-like bindings at creation.

solid answer

~40 s

`ref.instance.title = 'x'` is a plain property write: it bypasses input aliases, is not recorded for `ngOnChanges`, does not mark the component's view dirty, so an `OnPush` component (the default since v22) may never refresh, and it cannot work for signal inputs, which are read-only `InputSignal`s. `ref.setInput('title', 'x')` goes through Angular's input machinery: public name, transforms, `ngOnChanges`, dirty marking, and it skips a value identical to the last one. Since v20, `createComponent` also accepts `bindings`: `inputBinding('title', titleSignal)` re-reads a signal or getter whenever the host view is checked, `outputBinding('closed', handler)` listens to an output and is torn down with the component, and `twoWayBinding('expanded', writableSignal)` combines an input with the `expandedChange` output. The two styles do not mix: calling `setInput` on a ref created with input bindings throws NG0317 in development.

code

ts · 33 lines
ts
import {
  Component, ViewContainerRef, inject, input, model, output, signal,
  inputBinding, outputBinding, twoWayBinding,
} from '@angular/core';

@Component({
  selector: 'app-alert',
  template: `@if (expanded()) { <p>{{ text() }}</p> }
    <button type="button" (click)="dismissed.emit()">Dismiss</button>`,
})
export class Alert {
  readonly text = input.required<string>();
  readonly expanded = model(true);
  readonly dismissed = output<void>();
}

@Component({ selector: 'app-alert-host', template: `` })
export class AlertHost {
  private readonly vcr = inject(ViewContainerRef);
  readonly text = signal('Disk almost full');
  readonly expanded = signal(true);

  show(): void {
    const ref = this.vcr.createComponent(Alert, {
      bindings: [
        inputBinding('text', this.text),
        twoWayBinding('expanded', this.expanded),
        outputBinding('dismissed', () => ref.destroy()),
      ],
    });
    // ref.setInput('text', '...') here would throw NG0317 in development.
  }
}

go deeper

for a junior

Recall that inputs of a component created from code are set with setInput or bindings, not by assigning fields on the instance.

for a middle

Explain what setInput does that assignment does not: public names, transforms, ngOnChanges, dirty marking; and how inputBinding, outputBinding and twoWayBinding map to template syntax.

for a senior

Diagnose stale dynamic components under OnPush and zoneless defaults, choose bindings versus setInput per data source, and know the NG0315 and NG0317 development errors.

for a principal

Standardise how a codebase creates components from code, favouring declarative bindings so dynamic widgets behave like template children.

## Three ways to get data into a dynamic component When a component is created from code with `ViewContainerRef.createComponent` or the standalone `createComponent`, there is no parent template to bind its inputs. You have three options, and only two of them are correct in current Angular. ## 1. Assigning instance fields (avoid) `ref.instance.title = 'Revenue'` treats the component as a plain object: - **No input semantics.** Aliases and `transform` functions are ignored; you write the class property, not the public input. - **No `ngOnChanges`.** The change is not recorded, so logic keyed on input changes never runs. - **No dirty marking.** Components are `OnPush` by default since v22, and applications are zoneless by default since v21, so nothing schedules a check of the component's view; the screen can stay stale. - **Impossible for signal inputs.** An `input()` field is a read-only `InputSignal`; assigning to it replaces the signal object instead of setting a value, and TypeScript rejects it. ## 2. ComponentRef.setInput (imperative) `ref.setInput('title', 'Revenue')` uses the same path as a template binding: 1. Finds the input by its **public name**, so aliases work, and applies any transform. 2. Works for `input()`, `model()` and `@Input()` alike. 3. Records the change for `ngOnChanges`. 4. Marks the component's view dirty, which also schedules change detection. 5. Does nothing when the value is identical (`Object.is`) to the last one set. In development mode an unknown input name is reported. `setInput` is the natural fit when values arrive over time from imperative code, such as a WebSocket message updating a widget. ## 3. Creation-time bindings (declarative, v20+) Both creation APIs accept a `bindings` array built from helpers in `@angular/core`: | Helper | Template equivalent | Behaviour | |---|---|---| | `inputBinding('title', source)` | `[title]="source()"` | `source` is a signal or getter, re-read whenever the host view is checked | | `outputBinding('closed', fn)` | `(closed)="fn($event)"` | listener attached at creation, removed when the component is destroyed | | `twoWayBinding('expanded', sig)` | `[(expanded)]="sig"` | input from `sig`, plus `expandedChange` writing back into `sig` | A `directives` array can attach host directives with their own bindings in the same call. The result reads like a template, and because the source is a signal, updating the signal is all it takes to update the input. Two rules come from the implementation: - **Unknown names fail loudly in development.** An `inputBinding` naming a non-existent input throws NG0315. - **Do not mix styles.** Calling `setInput` on a `ComponentRef` that was created with input or two-way bindings throws NG0317 in development; choose one style per component. ## Choosing - Values that live in signals owned by the creator: **`inputBinding`** and **`twoWayBinding`**. - Events: **`outputBinding`**, instead of subscribing to `ref.instance.closed` by hand and remembering to unsubscribe. - Values pushed occasionally by imperative code, or components created without bindings (including through `NgComponentOutlet`, which uses `setInput` internally): **`setInput`**. - Never `ref.instance.x = ...` for inputs. ## Diagnosing a stale dynamic component 1. Check how the value reaches the component: an assignment on `ref.instance` is the usual culprit. 2. Replace it with `setInput`, or bind a signal with `inputBinding`, and confirm the view updates. 3. If the component was created with bindings, look for a leftover `setInput` call; development builds report NG0317 for the combination. 4. If a binding seems to do nothing, check the input's public name; with an alias the class property name will not match, and development builds report NG0315. 5. For outputs, confirm the listener was registered with `outputBinding` rather than a manual subscription that was never made or was already cleaned up. ## A note on NgComponentOutlet The template directive applies its `inputs` object with `setInput` on every check, which is why it respects aliases and OnPush. It has no bindings array and no outputs option, which is one reason to move to `createComponent` with `outputBinding` when events matter.

  • Why does ref.instance.count = 5 appear to do nothing on screen in a v22 app?
    Components are OnPush by default since v22 and the app is zoneless by default since v21. A plain property write neither marks the component's view dirty nor schedules change detection, so the view is not re-checked. `setInput` or an `inputBinding` backed by a signal does both.
  • What does twoWayBinding('expanded', sig) wire up?
    An input binding that reads `sig` into the `expanded` input, and an output listener on `expandedChange` that calls `sig.set()` with the emitted value. It is the programmatic form of `[(expanded)]="sig"` and requires a writable signal.

saying these in an interview costs you the question

  • Assigning ref.instance.title is equivalent to setInput, just shorter.
  • setInput takes the class property name, not the public input name.
  • inputBinding copies the value once at creation and never updates it.
  • You can freely combine inputBinding and setInput on one ComponentRef.
  • Outputs of dynamic components must be subscribed and unsubscribed manually.