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?
answer
- public names, dirty marking
- a signal input is read-only
- declare bindings at creation
- outputs cleaned up with the component
- value plus <name>Change
basics
~20 sAssigning 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 linesimport {
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
Recall that inputs of a component created from code are set with setInput or bindings, not by assigning fields on the instance.
Explain what setInput does that assignment does not: public names, transforms, ngOnChanges, dirty marking; and how inputBinding, outputBinding and twoWayBinding map to template syntax.
Diagnose stale dynamic components under OnPush and zoneless defaults, choose bindings versus setInput per data source, and know the NG0315 and NG0317 development errors.
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.