Skip to the solution
EasyFrontend10 min read

What is the purpose of ngOnChanges and how does it capture changes in @Input properties?

428 views

The problem

Question presented to candidate: "A child component needs to react when its parent passes it new data. Walk me through ngOnChanges: when does it run, what exactly is inside the SimpleChanges object, and when does it NOT fire even though you expected it to?"

What a strong answer should cover:

  • ngOnChanges runs when a parent template binding produces a new value for an input, compared by reference (===); the first call happens before ngOnInit.
  • SimpleChanges holds one SimpleChange per changed input, with previousValue, currentValue, firstChange (and isFirstChange()); on the first call previousValue is undefined.
  • Only bound inputs that changed appear; an input nobody binds never shows up.
  • It does not fire for in-place mutation of an object input, for the same value set again, or for code that assigns the property directly; ComponentRef.setInput() does trigger it.
  • With OnPush as the default in v22, a mutated object input does not even re-render the child.
  • Signal inputs are included in ngOnChanges too, but computed() / effect() are usually the better tool; SimpleChanges<T> gives typed keys.

Clarifying questions expected:

  • "Are these decorator @Inputs or signal input()s?"
  • "Is the parent passing objects it mutates, or new objects each time?"

Code / implementation expected: Yes β€” a child that logs its SimpleChanges, plus a demonstration of a mutation that does not trigger it.

  • lifecycle
  • inputs

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

10 min read Β· 10 sections

Read the code

AngularLogging SimpleChanges: new values, same value, mutation and replacement
import { Component, Input, OnChanges, SimpleChanges, signal } from "@angular/core";

type Filter = { tag: string };

@Component({
  selector: "user-badge",
  template: `
    <p><b>{{ name }}</b> score {{ score }} Β· tag {{ filter.tag }}</p>
    <ol>
      @for (line of log(); track $index) { <li>{{ line }}</li> }
    </ol>`,
})
export class UserBadgeComponent implements OnChanges {
  @Input() name = "";
  @Input() score = 0;
  @Input() filter: Filter = { tag: "-" };
  log = signal<string[]>([]);

  // Called before ngOnInit with every bound input, then again whenever a
  // parent binding produces a NEW value (compared with ===).
  ngOnChanges(changes: SimpleChanges<UserBadgeComponent>) {
    const lines = Object.entries(changes).map(
      ([key, c]) =>
        `${key}: ${JSON.stringify(c!.previousValue)} -> ${JSON.stringify(c!.currentValue)}` +
        ` (firstChange=${c!.firstChange})`,
    );
    this.log.update((old) => [...old, lines.join(" | ")]);
  }
}

@Component({
  selector: "app-root",
  imports: [UserBadgeComponent],
  template: `
    <user-badge [name]="name()" [score]="score()" [filter]="filter" />
    <button (click)="score.set(score() + 1)">score + 1</button>
    <button (click)="name.set(name())">same name again</button>
    <button (click)="filter.tag = 'mutated'">mutate filter</button>
    <button (click)="filter = { tag: 'new object' }">replace filter</button>`,
})
export class AppComponent {
  name = signal("Ada");
  score = signal(1);
  filter: Filter = { tag: "a" };
}
AngularngOnChanges with a signal input and a transformed input, next to computed()
import { Component, Input, OnChanges, SimpleChanges, booleanAttribute, computed, input, signal } from "@angular/core";

@Component({
  selector: "price-tag",
  template: `<span>{{ amount() }} {{ currency }} ({{ label() }}){{ compact ? "" : " incl. VAT" }}</span>`,
})
export class PriceTagComponent implements OnChanges {
  amount = input(0);                                       // signal input
  @Input() currency = "EUR";                               // decorator input
  @Input({ transform: booleanAttribute }) compact = false; // transformed input

  // The signal way: derive instead of reacting.
  label = computed(() => (this.amount() > 100 ? "pricey" : "cheap"));

  // ngOnChanges still fires for signal inputs too, and it sees
  // the value AFTER the transform ran ("" became true).
  ngOnChanges(changes: SimpleChanges<PriceTagComponent>) {
    for (const [key, c] of Object.entries(changes)) {
      console.log(`${key}: ${JSON.stringify(c!.previousValue)} -> ${JSON.stringify(c!.currentValue)} first=${c!.isFirstChange()}`);
    }
  }
}

@Component({
  selector: "app-root",
  imports: [PriceTagComponent],
  template: `<price-tag [amount]="amount()" compact />
    <button (click)="amount.set(150)">raise price</button>`,
})
export class AppComponent {
  amount = signal(10);
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 8 of 121 in Angular

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Angular questions
Up nextEasy

How do child components emit events to parents?

Continue