In Angular, why should a test or host code set a component's input with ComponentRef.setInput instead of assigning the field on componentInstance?
answer
- same path as a template binding
- read-only signal inputs
- marks OnPush views dirty
- uses the public alias
basics
~20 sComponentRef.setInput sets an input the way a template binding does: it works for signal inputs, runs transforms, triggers ngOnChanges and marks OnPush views for check. Assigning componentInstance fields does none of that and cannot set a read-only InputSignal.
solid answer
~40 sAssigning `fixture.componentInstance.size = 64` bypasses Angular: it cannot work for an `input()` signal, which is read-only, it skips the transform and `ngOnChanges`, and it does not mark the view for checking, which matters because components are `OnPush` by default since v22. `componentRef.setInput('size', 64)` goes through the input machinery like a template binding: it runs the transform, marks the component dirty, and `ngOnChanges` runs on the next check. It ignores a value identical to the last one by `Object.is`, and it takes the public name, so an aliased input is set by its alias. In tests the call is `fixture.componentRef.setInput(...)`, made before the first change detection when the input is required.
code
ts · 12 linesimport { TestBed } from '@angular/core/testing';
import { Avatar } from './avatar';
it('renders the requested size', async () => {
const fixture = TestBed.createComponent(Avatar);
fixture.componentRef.setInput('src', '/u/7.png');
fixture.componentRef.setInput('size', '64');
await fixture.whenStable();
const img: HTMLImageElement = fixture.nativeElement.querySelector('img');
expect(img.getAttribute('width')).toBe('64');
});go deeper
Know that tests set inputs with fixture.componentRef.setInput, not by assigning componentInstance fields.
Explain what setInput does that assignment cannot: transforms, ngOnChanges, marking OnPush views dirty, and alias names.
Fix flaky or silent tests after a move to signal inputs and the v22 OnPush default by routing every input write through setInput.
Standardise test helpers so input setting always goes through Angular's input path, making the suite robust to future change-detection defaults.
## The problem Two situations create an Angular component **without a parent template binding its inputs**: - a **test**, where `TestBed.createComponent(Avatar)` returns a `ComponentFixture`; - **programmatic creation**, where code obtains a `ComponentRef` and inserts it into the page. The old habit was to assign fields on the instance: `fixture.componentInstance.size = 64`. That no longer works well, for three reasons: 1. With **signal inputs** there is nothing to assign: `size` is an `InputSignal`, which is read-only, and replacing the field with a number breaks the component. 2. A direct assignment is invisible to Angular: it does **not** call `ngOnChanges` and does **not** mark the component for checking. 3. Since **v22** components are **`OnPush` by default**, so an unmarked component is not re-checked and the new value does not render. ## What ComponentRef.setInput does `ComponentRef.setInput(name, value)` sets an input exactly as a template binding would: - It writes through Angular's **input machinery**, so it works for `input()`, `model()` and `@Input()` alike, and it runs the input's **transform**. - It **marks the component view dirty**, so an `OnPush` component is checked on the next change detection. - The component's **`ngOnChanges`** runs with a `SimpleChanges` entry on the next check, as for a template binding. - It skips the update when the new value is identical, by `Object.is`, to the last value set, matching how template bindings behave. - It takes the input's **public name**, which is the **alias** if one is declared, not necessarily the class field name. - In development mode, a name that matches no input produces an error message telling you to declare it with `input()`, `model()` or `@Input()`. Tests reach it through the fixture: `fixture.componentRef.setInput('size', 64)`. ## A test that uses it ```ts import { TestBed } from '@angular/core/testing'; import { Avatar } from './avatar'; it('renders the requested size', async () => { const fixture = TestBed.createComponent(Avatar); fixture.componentRef.setInput('src', '/u/7.png'); fixture.componentRef.setInput('size', '64'); // numberAttribute transform runs await fixture.whenStable(); const img: HTMLImageElement = fixture.nativeElement.querySelector('img'); expect(img.getAttribute('width')).toBe('64'); }); ``` Setting `src` before the first check matters: `src` is `input.required()`, and a template read of an unset required input throws **NG0950**. ## Comparison | | `componentInstance.size = 64` | `componentRef.setInput('size', 64)` | |---|---|---| | Works with `input()` | no, the field is a read-only signal | yes | | Runs the input transform | no | yes | | Calls `ngOnChanges` | no | yes, on the next check | | Marks an `OnPush` view for check | no | yes | | Uses the alias | not applicable | yes, the public name | ## Aliases catch people out With `size = input(40, { alias: 'avatarSize' })`, the input's public name is `avatarSize`. `setInput('size', 64)` matches no input and, in dev mode, reports an error; `setInput('avatarSize', 64)` is correct. This mirrors the template, where the parent writes `[avatarSize]`. ## One restriction A component created programmatically **with the `inputBinding()` helpers** has its inputs managed by those bindings; calling `setInput` on it throws in development mode (*Cannot call `setInput` on a component that is using the `inputBinding` or `twoWayBinding` functions*). Choose one mechanism per component instance. How to create components from code, and those binding helpers, is a separate subject. ## Why interviewers ask it It checks whether a candidate has adapted testing and dynamic-rendering habits to signal inputs and the `OnPush` default: the answer "just assign the property" was acceptable for years and is now a bug.
- The avatar declares size = input(40, { alias: 'avatarSize' }); what does setInput('size', 64) do?It matches no input, because `setInput` uses the public name, which is the alias. In development mode Angular reports that it can't set the 'size' input and asks you to declare it. The correct call is `setInput('avatarSize', 64)`, mirroring `[avatarSize]` in a template.
- Does calling setInput twice with the same object re-run ngOnChanges?No. `setInput` remembers the last value per input and returns early if the new one is identical by `Object.is`, matching template bindings. Mutating that object and passing it again is therefore ignored; pass a new object instead.
saying these in an interview costs you the question
- Assigning componentInstance.size works the same as setInput for signal inputs.
- setInput takes the class field name even when the input has an alias.
- setInput skips the input's transform function.
- A direct field assignment triggers ngOnChanges.
- setInput re-applies an identical value and re-runs ngOnChanges.