Senior Angular Developer Interview Questions: Advanced Topics and Answers

Senior Angular developer interview questions on OnPush, signals vs RxJS, state management trade-offs, and zoneless defaults, with worked TypeScript examples.
Author
GreatFrontEnd Team
17 min read
Sep 24, 2026
Senior Angular Developer Interview Questions: Advanced Topics and Answers

Senior Angular developer interview questions rarely start with "what is a component." They start with a broken OnPush view, a 30-line RxJS chain a teammate wrote to toggle a boolean, or the question of when a signal in a service stops being enough and a real store is warranted. This guide works through 8 of those questions, each with a worked answer grounded in Angular's own documentation and the framework's actual mechanics, not folklore about what "senior" is supposed to mean.

What separates senior from mid-level Angular candidates

A mid-level candidate can explain what ChangeDetectionStrategy.OnPush does. A senior candidate can explain why a view under OnPush silently stopped updating after a service mutated a shared object, and can say what they'd check in Angular DevTools before reaching for a fix. The pattern repeats across every question below: a senior answer names the diagnostic step before the fix, states the condition under which a rule of thumb breaks, and can say "I haven't used that API day to day, but based on how it's built I'd expect X" rather than defaulting to a memorized shape. That is the register this guide is written in.

Question 1: A component under OnPush stops updating when a parent service changes its data. What happened, and how do you fix it?

How to approach it

ChangeDetectionStrategy.OnPush limits when Angular checks a component. A classic failure occurs when a parent passes an object or array to an OnPush child and then mutates that value in place: because the input value hasn't changed, Angular has no new-input notification to check that subtree. State held in a service is slightly different: Angular does not watch arbitrary service properties for reference changes, so the service should expose state through a notifying mechanism such as a signal or an Observable consumed with AsyncPipe, or explicitly mark the component for check.

// Breaks under OnPush: same array reference, no trigger fires
@Injectable({ providedIn: 'root' })
export class CartService {
private items: CartItem[] = [];
add(item: CartItem) {
this.items.push(item); // mutates in place
}
getItems() {
return this.items;
}
}
// Fixed: updating the signal notifies Angular that consumers need updating
@Injectable({ providedIn: 'root' })
export class CartService {
private itemsSignal = signal<CartItem[]>([]);
readonly items = this.itemsSignal.asReadonly();
add(item: CartItem) {
this.itemsSignal.update(current => [...current, item]);
}
}
The diagnostic step a senior answer leads with, before touching any code, is confirming this is actually the cause: opening Angular DevTools' component tree, checking which change detection strategy is active on the component, and checking whether the expected @Input or signal is actually changing reference. Guessing "I'd add OnPush" or "I'd remove OnPush" without that check is a pattern a practicing interviewer who writes about senior Angular rounds (https://blog.brecht.io/angular-interview-questions-for-seniors/) flags directly, because it treats a symptom fix as a diagnosis rather than confirming the cause. Using OnPush high in the component tree is not inherently a problem. What causes missed updates is code that changes state without one of Angular's notification mechanisms. In modern Angular, the more useful diagnostic question is whether the state change actually notified Angular that the affected view needed checking.

Since Angular v20.2, this class of problem has a second path: provideZonelessChangeDetection() shipped as a stable, production-ready API, and Angular's own zoneless guide (https://angular.dev/guide/zoneless) states plainly that "zoneless is the default in Angular v21+" for new applications, so you do not need to opt in. Zoneless removes Zone.js from the picture entirely and change detection runs off explicit signals and markers rather than patched async APIs, which makes the mutation-versus-replacement distinction above matter even more, not less, since there is no zone-driven fallback catching a missed update. Worth knowing for a version-aware answer: as of Angular v22 (https://angular.dev/roadmap), newly generated components default to OnPush rather than the old default strategy, which itself was renamed ChangeDetectionStrategy.Eager starting in v21.2 per the OnPush-by-default RFC (https://github.com/angular/angular/issues/66779), and ng update auto-migrates existing default-strategy components to that explicit name rather than silently reinterpreting them.

Question 2: Signals or RxJS, what's the actual mechanical difference?

How to approach it

The weak version of this answer is "signals are newer." The senior version names what actually differs mechanically: how each handles multiple dependencies updating together. A computed() signal is lazily evaluated and memoized. If multiple dependencies change before the computed value is read again, the next read recalculates it using all of their latest values. By contrast, combineLatest over independently emitting sources can push an intermediate emission after the first source changes and another after the second.

const firstName = signal('Ada');
const lastName = signal('Lovelace');
const fullName = computed(() => `${firstName()} ${lastName()}`);
firstName.set('Grace');
lastName.set('Hopper');
console.log(fullName()); // recomputes exactly once, reads "Grace Hopper"
RxJS's combineLatest on the same kind of dual-source update does not have that guarantee built in. Because each source stream emits independently, updating two BehaviorSubjects back to back can produce two separate emissions from the combined stream, one with the old value from the second source still attached, before settling on the fully updated pair, a pattern commonly described as the "glitched selector" problem in writing about signal migrations. It is the concrete reason to prefer a signal-based derivation for UI state specifically: state derived from multiple synchronous inputs, where a caller reading the value between updates would see an inconsistent one.

None of this makes RxJS the wrong tool. Angular's own signals guide (https://angular.dev/guide/signals) places RxJS interoperability, toSignal() and toObservable() from @angular/core/rxjs-interop, under its "Extended Ecosystem" documentation rather than a deprecation notice, and the framework team's position is coexistence: RxJS for genuinely asynchronous streams like WebSocket messages or HTTP polling, signals for synchronous UI state. The senior signal is knowing which one a given problem actually is. One scenario a senior-level Angular interview bank uses directly (https://arc.dev/employer-blog/angular-interview-questions-from-fundamentals-to-senior-level-scenarios/) tests this: a teammate writes a 30-line RxJS operator chain to toggle a boolean on a button click. A senior answer does not start optimizing the chain, it asks whether RxJS was the right tool for a synchronous boolean flip at all, and proposes the signal or plain property that would be easier for the next engineer to read and test.

Question 3: How would you model a dropdown that selects an item, but lets the user locally edit it before saving?

How to approach it

This is a real scenario a senior-level Angular interview bank uses (https://arc.dev/employer-blog/angular-interview-questions-from-fundamentals-to-senior-level-scenarios/) to test whether a candidate reaches for computed() reflexively. A computed() signal is read-only by construction, so it cannot represent "derived from the dropdown selection, but the user can then diverge from it locally." That's exactly the gap linkedSignal() (https://angular.dev/guide/signals/linked-signal) fills: it initializes from a source, same as computed(), but the result is a writable signal you can set() or update() directly, and it also has access to the previous value when the source changes again.

selectedShippingOption = linkedSignal<ShippingOption[], ShippingOption>({
source: this.shippingOptions,
computation: (newOptions, previous) => {
// keep the same option selected if it still exists, else fall back
return newOptions.find(opt => opt.id === previous?.value.id) ?? newOptions[0];
},
});
Once selectedShippingOption exists, calling selectedShippingOption.set(editedOption) after the user edits the panel works exactly as it would on a plain signal(), while the value still resets correctly if the underlying shippingOptions source changes. Naming linkedSignal unprompted, rather than reaching for a computed() plus a separate override signal() wired together by hand, is the specific signal an interviewer is checking for in this scenario.

Question 4: At what point does "just use a signal in a service" stop being good enough?

How to approach it

For state scoped to one component or shared across a couple of siblings, a plain injectable service exposing a signal() and a few update methods is usually the right amount of machinery, it is testable, requires no library, and needs no ramp-up for anyone already familiar with Angular services. The trade-off shifts as three things grow at the same time: the number of consumers, the complexity of derived state built from that state, and the amount of async coordination (loading flags, in-flight request cancellation, optimistic updates) layered on top.

NgRx's Signal Store (https://ngrx.io/guide/signals/signal-store) is built specifically for that middle ground, feature-level state that's shared but not application-global. It composes a store from withState(), withComputed(), and withMethods(), keeping declarative state, derived values, and update methods together rather than the action/reducer/selector/effect structure commonly used with classic @ngrx/store. The classic store still earns its place for genuinely global, event-driven state, authentication, a shopping cart shared across unrelated feature areas, anything where multiple independent parts of the app need to react to the same event stream in a coordinated way. This is the exact question one senior-level Angular interview bank (https://arc.dev/employer-blog/angular-interview-questions-from-fundamentals-to-senior-level-scenarios/) asks directly: at what point does "just use a signal in a service" stop being good enough.

The anti-pattern a senior answer should flag is reaching for either NgRx flavor to solve trivial component communication, passing a value from a sibling to a sibling, that a plain @Input()/@Output() pair or a two-line signal service already solves. That's boilerplate bought for a problem that didn't need it, and it's a pattern interviewer write-ups (https://blog.brecht.io/angular-interview-questions-for-seniors/) specifically call out as overengineering.

Question 5: @defer or route-level lazy loading, and where's the actual crossover point?

How to approach it

These solve the same underlying problem, initial bundle size, at different granularity. Route-level lazy loading (loadComponent on a route) splits at the navigation boundary: an entire page's code downloads only when the user navigates there. @defer operates inside an already-loaded template and can defer a single component within that page, based on viewport visibility, user interaction, a timer, or a custom condition.

@defer (on viewport) {
<product-reviews [productId]="productId()" />
} @placeholder {
<div class="reviews-placeholder">Reviews load as you scroll</div>
} @loading (minimum 200ms) {
<spinner />
}
The crossover point where @defer stops helping is over-splitting: Angular's own documentation (https://angular.dev/guide/templates/defer) on the feature is explicit that nested @defer blocks need different triggers, because blocks that all fire on the same condition, page load or viewport entry at the same scroll position, "cause cascading requests and may negatively impact page load performance," the same waterfall problem you'd get chaining sequential network calls instead of the parallel ones the app actually needs. The same guidance cautions against deferring content that's visible immediately on load, since that trades a slightly smaller initial bundle for a layout shift once the deferred content resolves, which is a Core Web Vitals cost, not a win. A senior answer distinguishes the two mechanisms by what they defer (a route versus a template region) rather than treating @defer as a drop-in replacement for lazy routes.

Question 6: Walk through hierarchical dependency injection and how it affects bundle size

How to approach it

@Injectable({ providedIn: 'root' }) (https://angular.dev/guide/di) is Angular's recommended default for most services specifically because it's tree-shakable: if nothing in the final build actually injects that service, the bundler can drop it entirely, because the provider registration lives with the service's own metadata rather than in a module's fixed provider array that always gets included. A component-level provider, declared in a component's own providers array, creates a new instance scoped to that component and its children instead of the application-wide singleton, which is the right call when you genuinely want per-instance state, a form-state service scoped to one dialog, for example, rather than a global one.

@Optional() changes what happens when a dependency isn't provided anywhere in that chain: instead of Angular throwing at injection time, the injected value is null, which matters for genuinely optional integrations, an analytics service that may or may not be configured in a given deployment, where a hard failure would be wrong. The senior-level framing ties this back to bundle size directly: a service provided at the component level but only actually used by one rarely-loaded component stays out of the main bundle if that component is also lazy-loaded, while a providedIn: 'root' service pulled in by something in the eagerly-loaded shell ships in the initial bundle regardless of where else it's used. Reasoning about where a provider lives, not just whether DI is used at all, is what separates "I know what dependency injection is" from "I can explain why this service ended up in the initial bundle."

Question 7: How does testing change with signals and Vitest, and where do you still need TestBed?

How to approach it

Angular's CLI now sets up Vitest as the default unit test runner (https://angular.dev/guide/testing) for new projects, and the framework's own Karma testing guide (https://angular.dev/guide/testing/karma) is precise about its status: it is "still a supported and widely used test runner," not removed, though new investment clearly goes toward Vitest and its migration schematic. One concrete casualty of that move worth naming specifically: fakeAsync and tick() depend on Zone.js. They are not available in Angular's default Vitest setup, which does not apply the Zone.js testing patch. Angular provides a zone.js/plugins/vitest-patch compatibility layer for migrating existing suites, but newer tests can instead use native async patterns or Vitest's fake timers. The replacement is the test runner's own fake-timer APIs (Vitest's vi.useFakeTimers()), which control time the same way but without depending on Zone.js patching.

For pure signal logic, a computed value, a signal-based service method, testing does not need TestBed or fixture.detectChanges() at all, because reading a signal is just calling a function:

it('derives the discounted total from the cart and the coupon', () => {
const service = new PricingService(); // plain instantiation, no TestBed
service.setCoupon('SAVE10');
service.addItem({ id: 1, price: 100 });
expect(service.total()).toBe(90); // signal read, synchronous, no fixture needed
});
That shortcut stops working the moment effect() or inject() is involved, both need Angular's injection context, which TestBed.runInInjectionContext() or a full TestBed setup still provides. Component tests that render a template still generally use TestBed and ComponentFixture, because the template has to go through Angular's rendering lifecycle. In zoneless tests, however, prefer letting Angular respond to normal change notifications and awaiting fixture.whenStable() when necessary rather than forcing fixture.detectChanges() after every state change. For components with a nontrivial internal structure, particularly a shared UI library component, Angular CDK's component test harnesses (https://material.angular.dev/cdk/test-harnesses) (ComponentHarness) let a test interact through the same kind of public API a user would, a click, a value read, rather than reaching into the component's DOM directly, which means the test survives an internal markup change that doesn't affect behavior.

Question 8: How would you plan a migration off AngularJS, and when is a full rewrite the wrong call?

How to approach it

AngularJS's own long-term support ended in January 2022 (https://endoflife.date/angularjs), so at this point this question tests migration judgment on a genuinely legacy codebase, not familiarity with a current framework. Angular's @angular/upgrade package (UpgradeModule) (https://v17.angular.io/guide/upgrade) is built for exactly this: running AngularJS and Angular in the same application simultaneously, bootstrapped together, so components can move over incrementally rather than all at once. The staged approach that de-risks this is bottom-up: migrate leaf components first, the ones with the fewest dependents, upgrade or downgrade services next so both frameworks can share state where needed, and leave routing and top-level shell code for last, since that's what everything else depends on and where a mistake has the widest blast radius.

 

A full rewrite is occasionally the right call, a codebase small enough that a parallel rebuild is genuinely faster than untangling a hybrid bridge, or one where the AngularJS code is so far from current patterns that the interop layer would cost more to maintain than it saves. But a senior answer treats that as a project-specific judgment made after sizing the app, not a default. The interview signal here is whether a candidate can reason about staged de-risking (what ships first, what's tested at each stage, how rollback works if a migrated section breaks) rather than jumping straight to "rewrite it," which is the answer that sounds decisive and is usually wrong for anything beyond a small app.

Common mistakes and red flags at the senior level

  • Proposing "I'd add OnPush" as a fix without profiling first. Guessing at a change detection strategy without checking Angular DevTools' component tree or confirming what's actually re-rendering is treated as a red flag by interviewers who write about these rounds, because it can mask the real bug rather than fix it.
  • Treating OnPush as a performance fix by itself. OnPush changes when Angular checks a subtree, but it does not replace profiling or fix inefficient rendering and state-management patterns on its own.
  • Reaching for NgRx, classic or Signal Store, for trivial component communication that a plain @Input()/@Output() pair or a two-line signal service already handles.
  • Treating unmanaged subscriptions as a one-off bug rather than a systemic pattern worth fixing at the architecture level, since a single missed takeUntilDestroyed() rarely stays single in a codebase that hasn't standardized on one unsubscription pattern.
  • Fixing a bad RxJS chain instead of questioning whether RxJS was the right tool. Fluent operator chaining can make a chain that's solving the wrong problem look more sophisticated rather than simpler.
  • Confusing where a provider lives with whether dependency injection is being used at all. A shaky grasp of providedIn: 'root' versus component-level providers, and what that does to singleton behavior and bundle size, is a gap the interviewer sources above call out even in candidates who otherwise use DI correctly.

Frequently asked questions

Do I need production experience with NgRx Signal Store specifically to answer the state management question well? No. What the question is testing is whether you can reason about the boilerplate, control, and encapsulation trade-off as team size and async coordination needs grow, not whether you've shipped with that exact library. Naming the axis correctly matters more than naming the API surface from memory.

Is RxJS being phased out of Angular in favor of signals? No, and a senior answer should resist that framing. Angular's own signals documentation treats toSignal()/toObservable() RxJS interop as part of the supported ecosystem, not a deprecation path, and the two are positioned for different jobs: RxJS for asynchronous streams, signals for synchronous UI state.

How current do I need to be on the latest Angular version to answer these well? Current enough to know the direction (zoneless by default, OnPush by default, signals as the primary reactivity primitive), not necessarily the exact minor version. If you're asked about a specific version number you're unsure of, saying so and reasoning from the direction of travel is stronger than guessing a number.

How to prepare

If the fundamentals, what @Input/@Output actually do, how a module or standalone component bootstraps, the basic shape of dependency injection, aren't already solid, GreatFrontEnd's Angular interview questions for experienced developers (https://www.greatfrontend.com/blog/angular-experienced-interview-questions) is the right starting point before tackling the judgment-level questions here. From there, the highest-value practice is picking a real Angular codebase you've worked in and being able to state, out loud, why a specific component uses OnPush or doesn't, why a piece of state lives in a service versus a store, and what would break if you swapped one for the other. The trade-off reasoning here carries over directly to GreatFrontEnd's guides on senior Redux (https://www.greatfrontend.com/blog/senior-redux-developer-interview-questions-advanced-topics-and-answers), senior testing (https://www.greatfrontend.com/blog/senior-testing-developer-interview-questions-advanced-topics-and-answers), and senior GraphQL (https://www.greatfrontend.com/blog/senior-graphql-developer-interview-questions-advanced-topics-and-answers) interview questions, which apply the same judgment-first pattern to different technical domains.

Conclusion

Senior Angular developer interview questions test whether you can reason about a framework's mechanics under pressure, not whether you can recite an API surface. Diagnosing an OnPush failure with Angular DevTools before guessing at a fix, naming the actual batching difference between a computed signal and a combined RxJS stream, knowing when a plain signal service stops covering a state problem, and treating a legacy AngularJS migration as a staged de-risking exercise rather than an automatic rewrite are the differentiators that show up across these questions. What separates a senior answer from a mid-level one is having owned the consequences of these decisions on a real codebase, not just being able to describe them correctly.

Related articles

Senior Redux Developer Interview Questions: Advanced Topics and AnswersSenior Redux developer interview questions and answers: Redux Toolkit internals, memoized selectors, RTK Query trade-offs, and when Redux is overkill
Senior Testing Developer Interview Questions: Advanced Topics and AnswersSenior testing developer interview questions on flaky tests, mocking, coverage, and E2E architecture, with worked answers and real, sourced examples throughout.