Change detection, streams and DI
Most Angular traps come from reference equality (OnPush and signals), cold observables, forgotten subscriptions and provider scope. Explain the rule, then the fix.
- RenderingOnPush, signals and ExpressionChanged errors.
- RxJSCold observables and leaks.
- DIProvider scope and singletons.
Angular bugs usually come from change detection, observables and dependency injection rules. These tricky questions show the code, what happens and why — exactly what senior Angular interviews probe.
1. Why does the OnPush component not update?
The parent mutates the array it passes in, so the input reference stays the same and OnPush does not check the component. Pass a new array (or use signals).
// parent
this.orders.push(newOrder); // child with OnPush does not update
this.orders = [...this.orders, newOrder]; // new reference → child updates2. Why does the signal-based view not update?
Mutating the object inside a signal keeps the same reference, so dependents may not be notified. Create a new value with update.
user = signal({ name: 'Asha', city: 'Pune' });
move() {
this.user().city = 'Delhi'; // no reliable update
this.user.update(u => ({ ...u, city: 'Delhi' })); // correct
}3. What causes ExpressionChangedAfterItHasBeenCheckedError?
In development, Angular checks bindings twice; if a value changes between the checks — typically set in ngAfterViewInit or by a child changing parent state during change detection — it throws. Move the change earlier, derive it with a computed signal, or schedule it after the current check.
4. Why is the HTTP request sent twice?
HttpClient observables are cold: each subscription sends a request. Subscribing in code and using the async pipe on the same observable (or two async pipes) triggers two calls. Share it with shareReplay, or convert once with toSignal.
users$ = this.http.get<User[]>('/api/users');
// template uses users$ | async twice → two requests
users$ = this.http.get<User[]>('/api/users').pipe(shareReplay({ bufferSize: 1, refCount: true }));
users = toSignal(this.http.get<User[]>('/api/users'), { initialValue: [] });5. Why is this a memory leak?
The interval subscription is never unsubscribed, so it keeps running (and holding the component) after the component is destroyed. Use takeUntilDestroyed(), the async pipe or toSignal.
ngOnInit() {
interval(5000).subscribe(() => this.refresh()); // leaks
}
constructor() {
interval(5000).pipe(takeUntilDestroyed()).subscribe(() => this.refresh()); // safe
}6. Why do two components get different instances of the “same” service?
The service is provided in a component's providers array (or a lazy route), which creates a new instance for that subtree. Use providedIn: 'root' for an app-wide singleton.
7. What does this template render while loading?
Nothing for the list, and no error — the async pipe emits null until the first value. Handle the loading state explicitly with @if and an @else block.
@if (users$ | async; as users) {
@for (u of users; track u.id) { <p>{{ u.name }}</p> }
} @else {
<app-spinner />
}8. Why does calling a method in the template hurt performance?
Template expressions are evaluated on every change detection pass; an expensive method like {{ calculateTotal() }} runs constantly. Use a computed signal or a pure pipe so it recalculates only when inputs change.
Remember
Key points
- New references trigger checks
For OnPush and signals.
- HttpClient is cold
Each subscribe sends a request.
- takeUntilDestroyed()
For long-lived streams.
- providedIn: root
One instance app-wide.
Interview drill
Follow-up questions
What the interviewer asks after your first answer. Turn on practice mode, answer each one aloud, then reveal.
-
1 Does markForCheck() run change detection immediately? Intermediate
No — it marks the component and its ancestors to be checked in the next cycle; detectChanges() runs it synchronously.
-
2 Why does ngOnChanges not fire when I mutate an input object? Intermediate
It fires only when the input reference changes.
-
3 What happens if you forget track in @for? Basic
It is required — the template will not compile without it.
-
4 Why does effect() complain when created in ngOnInit? Advanced
effect() needs an injection context; create it in the constructor or a field initialiser, or pass an injector.
-
5 Why does a Subject lose values for late subscribers? Basic
A plain Subject does not replay; use BehaviorSubject or ReplaySubject (or a signal) for current state.
-
6 Why is my lazy-loaded route's service a separate instance? Advanced
Providers declared on a lazy route create a child environment injector; services provided there are separate from root ones.