AcademyAngular
OnPush Change Detection and model()
OnPush skips a subtree until an @Input() reference actually changes; model() gives a component a two-way custom binding without a separate @Output() wiring.
2 min readUpdated 3 exercises
OnPush checks references, not contents
changeDetection: ChangeDetectionStrategy.OnPush tells Angular to skip re-checking a component unless one of its @Input() bindings received a genuinely new reference, an event fired inside it, or it was told explicitly to check. The performance win is real — a large tree stops being walked on every tick — but it demands a discipline default change detection never required: never mutate an object or array in place if a component depending on it is OnPush.
// mutation the parent's OnPush child will never see
this.score.value++;
// replacement the parent's OnPush child WILL see
this.score = { ...this.score, value: this.score.value + 1 };A score badge that "never updates" under OnPush is exactly the first line: the parent mutated the same object it always held, so the reference OnPush compares against never changed, and the child has no way to know anything happened. Replacing the object — a new reference every time the value changes — is the fix, and it is the same instinct as returning a new array instead of .push()ing onto the old one anywhere else in the codebase.
model() is a two-way custom binding without hand-wired @Output()
Before model(), a two-way custom binding meant an @Input() plus a same-named @Output() suffixed Change, wired by hand on both sides. model() collapses that into one signal-backed input that also knows how to write back to the parent:
value = model(0);
increment(): void {
this.value.update((n) => n + 1);
}<app-counter [(value)]="total" /> in the parent then genuinely two-way binds — the child's .update() calls propagate back out automatically. A counter whose buttons "work locally but never reach the parent" is one still built on a plain @Input(): incrementing it updates the child's own copy, because a plain @Input() was never wired to emit anything back, which is precisely the wiring model() exists to remove the need for.
What interviewers check
Whether "never mutate what an OnPush child reads" is a reflex, not something you have to be reminded of mid-debugging, and whether you know model() replaces the old @Input()-plus-@Output()-Change pattern rather than being an unrelated, newer API.
Now practice it
1 quiz · 2 drillsReading this page is the cheap half. These are the exercises that make you use it.
- OnPush Change DetectionProFix a score badge that never updates under OnPush because its parent mutates the same object instead of replacing it.DrillSolve · HardDrillSolve · Hard
- Two-way Custom Binding — value + valueChangeProFix a counter whose +/- buttons work locally but never actually reach the parent’s [(value)] binding.DrillSolve · HardDrillSolve · Hard
- Router · Guards & Lazy LoadingGuard and lazy-loading questions at the level where change-detection cost and route boundaries start to interact.QuizTake · MediumQuizTake · Medium
Read next
Generic Pipes and Type Guards
A pipe that stays generic over its input works for any array shape; a runtime type guard is the only thing standing between an untyped API response and a crash.
2 min
Compound Components and Dynamic Outlets
A compound component's parts coordinate through a shared, injected service instead of local state; NgComponentOutlet renders whichever component a mapping picks at runtime.
2 min