An Angular component with OnPush change detection has stopped updating when its data changes. Walk me through why, and how Vue avoids the same problem.
OnPush checks a component only when an input arrives with a new reference, a template event fires, an async pipe or signal it reads emits, or you mark it. Mutating an object in place satisfies none of those, so the view never gets checked.
What the interviewer is scoring
- Does the candidate enumerate what marks an OnPush view dirty rather than saying it checks less often
- Whether in-place mutation is identified as the cause instead of blaming a missing subscription
- That markForCheck and detectChanges are distinguished by what they schedule and where they run
- Whether the answer reaches for immutability or signals rather than sprinkling manual checks
- Does the candidate explain why Vue's read-tracking model does not have this failure mode
Answer
What OnPush promises, and what it withholds
The default change detection strategy checks every component in the tree on every cycle, which is correct but wasteful: a keystroke anywhere causes every binding in the application to be re-evaluated. ChangeDetectionStrategy.OnPush trades that blanket correctness for a contract. The component will be checked only when one of a small number of things happens, and if none of them happens the view keeps rendering the values it last saw, however stale the underlying data has become.
The list is short enough to recite, and reciting it is most of the answer. An OnPush view is checked when an input binding is set to a value that is not reference-identical to the previous one, when an event handler bound in that component's own template fires, when an async pipe in the template receives a new value, when a signal read by the template changes, and when something calls markForCheck on its ChangeDetectorRef. That is the whole set. Anything else that changes data leaves the view untouched.
In-place mutation satisfies none of the triggers
So the bug is almost always the same shape. A parent holds an array or an object, passes it into an OnPush child as an input, and then a handler pushes an item onto the array or assigns to a property. The data is genuinely different, the child's template genuinely reads the changed field, and the reference the input was bound to has not moved. Angular compares the new input value with the old one using reference identity, sees the same object, and declines to mark the child dirty.
// Same array reference, so the OnPush child is never marked dirty.
this.rows.push(newRow);
// New reference, so the input comparison sees a change.
this.rows = [...this.rows, newRow];
The second version is not a trick to fool the framework. It is the discipline OnPush is asking for: if a component may only be re-checked when its inputs change identity, then the inputs have to be values you replace rather than containers you edit. That is why OnPush and immutable update patterns are usually adopted together, and why adopting OnPush alone into a codebase full of in-place mutation produces a long tail of views that update at seemingly random moments, whenever some unrelated event happens to fire inside them.
The second common variant is data arriving from outside the component tree entirely. A service holds state and a WebSocket message or a timer callback writes to it. No input was set, no template event fired, and if the value is not exposed as an observable consumed through async or as a signal read in the template, nothing marks anything. Exposing service state as an observable or a signal is what turns those updates back into one of the recognised triggers.
markForCheck and detectChanges are not interchangeable
When you do need to intervene manually, the two methods on ChangeDetectorRef mean different things and choosing the wrong one produces confusing results. markForCheck flags this view and every ancestor up to the root as needing to be checked, so the change is picked up during the next change detection cycle. It schedules nothing itself; it makes the view eligible.
detectChanges runs a check of this view and its children synchronously, right now. That is occasionally what you want, but it runs outside the ordinary cycle, so in development you will meet the error about an expression having changed after it was checked if you use it to patch up bindings from a lifecycle hook. Reaching for detectChanges repeatedly is a sign the data flow is wrong rather than the change detection.
Vue does not have this failure because it tracks reads
The contrast is worth drawing because it explains what kind of problem OnPush is. Angular's strategy is a coarse invalidation rule applied per component: something told me this subtree might be stale, so re-evaluate all of its bindings. Vue instead records, during render, which reactive properties a component read, and when one of those properties is written it re-renders exactly the components that read it. Nobody has to declare which inputs matter, and mutating a property of a reactive object is a tracked write, so push on a reactive array updates the view.
Vue's version of the trap is elsewhere: reactivity is carried by the proxy, so pulling a value out of a reactive object into a plain local variable gives you a snapshot with no connection back to the source, which is why toRefs exists and why ref values are read through .value. The lesson to draw out loud is that both frameworks make you keep the identity of the thing being observed intact, and the bug in each case is code that quietly steps outside the observation mechanism.
Angular's signals move it towards the Vue model, because a signal read during rendering registers that view and a write marks it dirty regardless of inputs. That narrows the space where the OnPush mutation bug can exist, but it does not remove it: any component still taking a plain object input under OnPush follows the old reference-comparison rule.
OnPush is a promise you make to the framework, not a performance flag you switch on. The moment code mutates a bound object in place, the promise is broken and the only surprising thing is which views happen to refresh anyway.
Likely follow-ups
- A WebSocket handler in a service updates a field on a shared object. Which components re-render and which do not?
- You call detectChanges in a lifecycle hook and get an error about an expression changing after it was checked. What did you do wrong?
- Reading a signal in a template marks that view dirty. What does that change about whether you still need OnPush at all?
- In Vue, when does destructuring a reactive object lose reactivity, and what do you use instead?
Related questions
- Angular, Vue and React are answers to the same problems. Compare how each handles change detection, reactivity and dependency injection.hardAlso on angular and vue5 min
- Draw me the boundary between your monitoring system and the plant's safety system. How would you know if the safety layer had been altered?hardAlso on change-detection6 min
- Your Product Backlog has grown past four hundred items and refinement takes two hours a week without anything getting clearer. What would you change?mediumSame kind of round: concept6 min
- You read a four gigabyte CSV into pandas and the process needs far more memory than that. Where did it go?mediumSame kind of round: concept4 min
- A list of a million small objects uses far more memory than the data inside them. Where is it going?mediumSame kind of round: concept5 min
- You cached something in a module-level dictionary. It works on your laptop and behaves oddly in production. What is different?mediumSame kind of round: concept5 min
- A promise rejects and nothing is awaiting it. What does Node do?hardSame kind of round: concept4 min
- Implement a FIFO queue using only two stacks, then tell me what a dequeue costs.mediumSame kind of round: concept4 min