skip to content

In Angular, why should a test or host code set a component's input with ComponentRef.setInput instead of assigning the field on componentInstance?

level: middleimportance: should knowfreq 40%

answer

  1. same path as a template binding
  2. read-only signal inputs
  3. marks OnPush views dirty
  4. uses the public alias

basics

~20 s

ComponentRef.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 s

Assigning `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 lines
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');
  await fixture.whenStable();

  const img: HTMLImageElement = fixture.nativeElement.querySelector('img');
  expect(img.getAttribute('width')).toBe('64');
});

go deeper

for a junior

Know that tests set inputs with fixture.componentRef.setInput, not by assigning componentInstance fields.

for a middle

Explain what setInput does that assignment cannot: transforms, ngOnChanges, marking OnPush views dirty, and alias names.

for a senior

Fix flaky or silent tests after a move to signal inputs and the v22 OnPush default by routing every input write through setInput.

for a principal

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.