In Angular, when should a component react to input changes with ngOnChanges and SimpleChanges, and when with computed() over a signal input?
answer
- a hook versus a derived signal
- runs before ngOnInit the first time
- previousValue and firstChange
- reference change, not mutation
basics
~20 sUse computed() over signal inputs to derive state declaratively; it stays current as any input changes. Use ngOnChanges when you need the previous value, the first-change flag, or still have @Input fields, since SimpleChanges carries previousValue, currentValue and firstChange.
solid answer
~40 s`ngOnChanges` is called with a `SimpleChanges` map whenever bound inputs get a new reference: before `ngOnInit` the first time, then on each change, and before the component's template is checked. Each `SimpleChange` has `previousValue`, `currentValue` and `firstChange`, and only changed inputs appear. With signal inputs the idiomatic way to derive state is `computed()`: `initials = computed(() => toInitials(this.name()))` is declarative, combines several inputs without key juggling, and cannot go stale. I keep `ngOnChanges` for decorator inputs and for logic that needs the previous value or the first-change flag. Neither fires when the parent mutates an object in place, because Angular compares input values with `Object.is`.
code
ts · 21 linesimport { Component, OnChanges, SimpleChanges, computed, input } from '@angular/core';
@Component({
selector: 'app-avatar',
template: `<span [class]="sizeClass()">{{ initials() }}</span>`,
})
export class Avatar implements OnChanges {
readonly name = input('');
readonly size = input(40);
readonly initials = computed(() =>
this.name().split(' ').map((p) => p.charAt(0)).join('').toUpperCase(),
);
readonly sizeClass = computed(() => (this.size() >= 64 ? 'avatar-lg' : 'avatar-sm'));
ngOnChanges(changes: SimpleChanges<Avatar>) {
const size = changes.size;
if (size && !size.firstChange) {
console.debug(`avatar size ${size.previousValue} -> ${size.currentValue}`);
}
}
}go deeper
Know that ngOnChanges receives SimpleChanges with previous and current values, and runs before ngOnInit the first time.
Explain reference-based change detection for inputs, the firstChange flag, and why computed() replaces most ngOnChanges derivations with signal inputs.
Refactor hook-based derivations into computed signals, and keep ngOnChanges only where previous values or first-change logic really matter.
Guide a team's migration away from imperative input handling, balancing readability gains against churn in stable components.
## Two ways to react when an input changes A component often needs something **derived** from its inputs: an avatar's initials from `name`, its CSS size class from `size`. Angular gives two mechanisms. **`ngOnChanges`** is a lifecycle hook, from the `OnChanges` interface. Angular calls it with a `SimpleChanges` object whenever one or more **bound inputs** receive a new value: - it runs **before** `ngOnInit` the first time, and again on each later change; - it runs **before** the component's own template is checked, so state set there is rendered; - it only fires for values set through a template binding or `ComponentRef.setInput`, not when the class assigns its own field; - "new value" means a different **reference** by `Object.is`: mutating an object in place does not trigger it. **`computed()`** over a **signal input** is the signal-era alternative: a derived signal that reads inputs by calling them and is recomputed when any of them changes. ## What SimpleChanges carries `SimpleChanges` maps each changed input's name to a **`SimpleChange`** with: | Member | Meaning | |---|---| | `previousValue` | the value before this change (`undefined` the first time) | | `currentValue` | the new value | | `firstChange` | `true` on the first change, before `ngOnInit` | | `isFirstChange()` | method returning the same flag | Only inputs that changed appear in the object, so code must check `if (changes['name'])` before using an entry. Since **v21** `SimpleChanges` is generic: `SimpleChanges<Avatar>` types each key and unwraps signal inputs to their value type. ## Side by side ```ts import { Component, Input, OnChanges, SimpleChanges, computed, input } from '@angular/core'; // Decorator inputs: derive in ngOnChanges. @Component({ selector: 'app-legacy-avatar', template: `<span>{{ initials }}</span>` }) export class LegacyAvatar implements OnChanges { @Input() name = ''; initials = ''; ngOnChanges(changes: SimpleChanges<LegacyAvatar>) { if (changes.name) { this.initials = toInitials(changes.name.currentValue); } } } // Signal inputs: derive with computed(). @Component({ selector: 'app-avatar', template: `<span>{{ initials() }}</span>` }) export class Avatar { readonly name = input(''); readonly initials = computed(() => toInitials(this.name())); } function toInitials(name: string): string { return name.split(' ').map((part) => part.charAt(0)).join('').toUpperCase(); } ``` ## Why computed() is usually better with signal inputs 1. **One place, declarative.** The derivation is written where the field is declared, not in a hook that must copy values into mutable fields. 2. **Multiple inputs compose.** `computed(() => sizeClass(this.size(), this.shape()))` updates when either input changes; with `ngOnChanges` you must handle every combination of keys present in `changes`. 3. **No stale fields.** A field assigned in `ngOnChanges` is only as fresh as the last hook call; a computed signal is always derived from the current inputs. 4. **Lazy and cached.** It recalculates only when read after a dependency changed. ## When ngOnChanges is still the right tool - **You need the previous value**, for example to animate from the old size to the new one, or to reset internal state only when a specific ID changes. `previousValue` gives it directly. - **You need to know it was the first change**, via `firstChange`. - **The component still uses `@Input` fields**, where there is no signal to derive from. - **Several inputs must be handled as one batch** in imperative code: Angular delivers all inputs changed in the same check in a single call. Side effects on input change, such as starting a request, are a separate subject; neither `computed()` nor `ngOnChanges` is the preferred home for those in signal-based code. ## Common mistakes - Assuming `ngOnChanges` fires when the parent mutates a property of an object input: the reference did not change, so neither the hook nor a `computed()` reading that input sees a change. - Reading `changes.name.currentValue` without checking `changes.name` exists. - Keeping a hand-maintained copy of an input in a field and updating it in `ngOnChanges`, when a `computed()` would remove the copy entirely.
- Does ngOnChanges fire when the parent pushes an item into an array bound to an input?No. Angular compares the new binding value with the old one using `Object.is`; the array reference is unchanged, so the input is not updated and neither `ngOnChanges` nor a `computed()` reading that input reacts. The parent should bind a new array instead of mutating the existing one.
- Does ngOnChanges run for signal inputs?Yes. `ngOnChanges` receives changes for signal inputs too, and a generic `SimpleChanges<Avatar>` types each entry by the signal's value type. It is simply rarely needed, because `computed()` covers most derivations.
- In what order do ngOnChanges and ngOnInit run on creation?When the component has bound inputs, the first `ngOnChanges` runs before `ngOnInit`, so the initial input values are already set when `ngOnInit` runs. Later changes call `ngOnChanges` again, while `ngOnInit` runs only once.
saying these in an interview costs you the question
- ngOnChanges fires when a property inside an object input is mutated.
- ngOnChanges runs after ngOnInit on the first binding.
- SimpleChanges contains every input on every call.
- ngOnChanges does not work with signal inputs at all.
- A computed() over an input must be refreshed manually in ngOnChanges.