Angular interviews in 2026 test v20 through v22, where zoneless change detection is the default for new apps from v21 and
*ngIfis deprecated in favour of@if. A loop usually breaks into three rounds: a concept round on framework mechanics, a machine-coding round where you build a working component under a timer, and a code-review round on somebody else's component. The answers that moved furthest are change detection, template control flow, and data loading, so a candidate who preps from a 2022 question list will confidently describe syntax the compiler now flags as deprecated.
How an Angular Interview Is Actually Structured
Most Angular loops decompose into three kinds of round. Names and order vary by company, and small teams often merge two of them into one call, but the skills being probed are distinct.
The concept round is verbal question and answer on framework mechanics, usually with a shared editor open but nothing to run. This is where "what does providedIn: 'root' do" and "explain switchMap versus mergeMap" live. It rewards precise short answers and one line of code on the screen.
The machine-coding round gives you a requirement and 30 to 60 minutes to build something that works: a typeahead, an accordion, a table. The interviewer watches you type. Once you have something rendering, they add a constraint that tests whether your first design can absorb it.
The code-review round hands you a component that compiles and asks what is wrong with it. Memory leaks, state that should be derived, a missing track, a subscription inside ngOnInit with no teardown.
The next four sections give concept-round answers. The machine-coding section covers round two in detail, with the four builds that come up most and what each one is grading. The section on stale answers cuts across all three rounds, because a deprecated habit will cost you in the concept round as a wrong answer, in the machine-coding round as code the interviewer has to correct, and in the code-review round as a defect you fail to spot.
One frame to set before any of it. Angular v22.0.0 was released on 2026-06-03, v21.0.0 on 2025-11-19, and v20.0.0 on 2025-05-28. As of v22, Angular ships a major every 12 months rather than every 6, with 4 to 6 minors per major and a patch release roughly weekly. From v22 on, each major gets about 24 months of support, 12 active and 12 LTS; v20 and v21 are still on the older 18-month window of 6 active and 12 LTS. "Angular in 2026" means v20 to v22, and the differences between those three versions are exactly what a good interviewer probes.
The Fundamentals Every Round Opens With
"What is a component?" A class with a template and a selector, declared with the @Component decorator. The interesting follow-up is what standalone changed. Angular v19 made standalone: true the default for components, directives and pipes, so you no longer write it by hand. NgModules were not deprecated. You opt back into one with standalone: false, an ng update migration adjusts existing code, and the strictStandalone compiler option enforces standalone-only if a team wants it. Saying "NgModules are dead" is wrong and interviewers notice.
A component written the way v22 expects, with no standalone flag and no decorator inputs:
@Component({
selector: 'user-card',
template: `<h2>{{ fullName() }}</h2>`,
})
export class UserCard {
first = input.required<string>();
last = input('');
fullName = computed(() => `${this.first()} ${this.last()}`.trim());
}
input.required<T>() enforces that a consumer supplies a value, validated at build time. input() also takes an alias option to rename the template binding and a transform to coerce incoming values, with booleanAttribute and numberAttribute built in. Angular recommends the signal-based input function for new projects while the decorator-based @Input API remains fully supported, and the same pairing holds for output() versus @Output.
The third one to have ready is model(), because it is what "two-way binding on a custom component" means in 2026. A model input is a writable signal that is both an input and an output: the output name is the input name with Change appended, so a consumer writes [(openIndex)].
@Component({
selector: 'accordion',
template: `<ng-content />`,
})
export class Accordion {
openIndex = model(0);
toggle(i: number) {
this.openIndex.update(open => (open === i ? -1 : i));
}
}
That is the shape the accordion and multi-step-form builds in the machine-coding round are reaching for: state the parent can read and set, owned in one place.
"How does dependency injection work?" A service marked @Injectable({providedIn: 'root'}) gets a single application-wide instance and is tree-shakable: if nothing injects it, it does not ship. Retrieve it with the inject() function rather than a constructor parameter in new code. The v22 addition worth naming is @Service, a shorthand for exactly that root-provided singleton, with one constraint: it supports inject() only, so there is no constructor injection at all. v22 also adds injectAsync for dependencies you want to load lazily.
@Injectable({providedIn: 'root'})
export class UserStore {}
export class UserCard {
private store = inject(UserStore);
}
The follow-up is injection context. inject() only works where Angular is actively constructing something: field initializers and constructors of DI-created classes, factory functions, and code run inside runInInjectionContext. Call it from a click handler and it throws. This matters again in the RxJS section, because takeUntilDestroyed() has the same requirement.
"Walk me through the lifecycle hooks." ngOnChanges runs when an input changes and receives a SimpleChanges map, and the docs are explicit that this "includes both signal-based and decorator-based inputs", so it still fires if you use input(). ngOnInit runs once after the first inputs are set, for setup that needs those inputs. ngAfterViewInit is when decorator-based view queries have resolved. ngOnDestroy is teardown.
The 2026 half of that answer is which ones you still reach for. ngOnInit and ngOnDestroy survive. ngOnChanges is largely displaced by a computed over signal inputs, which reacts to exactly the input you derive from instead of every input on the component. ngAfterViewInit is displaced by signal queries: viewChild, viewChildren, contentChild and contentChildren return signals that reflect the most up-to-date results, and viewChild.required reports an error if nothing matches rather than handing you undefined. The decorator query APIs remain fully supported.
For work that genuinely needs the DOM after paint, name afterNextRender and afterEveryRender. Both run on browser platforms only, never on the server, and both take phased callbacks that run in the order earlyRead, write, mixedReadWrite, read. The difference is the schedule, which is what the rename was for: afterNextRender runs once, after the next render, while afterEveryRender runs after every one. afterEveryRender has been stable since v20.
Signals, Change Detection, and Zoneless
This is the section that separates a 2026 answer from a 2022 one.
"What is a signal?" A value container that tracks who reads it. signal() holds writable state. computed() derives from other signals, and the property worth naming out loud is that computed signals are both lazily evaluated and memoized: the derivation runs on first read, then the cached value is served until a dependency changes. effect() runs a side effect when its dependencies change, and it is for effects, not for copying one signal into another. If you find yourself writing an effect that sets a signal, the answer is computed or linkedSignal.
linkedSignal is the one candidates miss. It creates writable state that is intrinsically linked to other state, so a user's selection can survive until the thing it points at goes away:
options = input.required<ShippingOption[]>();
selected = linkedSignal({
source: this.options,
computation: (opts, previous) =>
opts.find(o => o.id === previous?.value?.id) ?? opts[0],
});
A user can set() selected directly, and when options changes the computation runs again with access to the previous value, which keeps the selection valid.
The shape of that trap is ordinary JavaScript, and you can demonstrate it without Angular at all:
let tracking = true;
function read(name) {
console.log(`${name}: ${tracking ? 'tracked' : 'untracked'}`);
}
async function effectBody() {
read('userId');
await Promise.resolve();
read('pageSize');
}
effectBody();
tracking = false;
userId: tracked
pageSize: untracked
Whatever context was active during the synchronous part is gone by the time the continuation resumes. An effect that reads this.pageSize() after an await simply will not re-run when pageSize changes, and the bug looks like a component that updates on Tuesday and not on Wednesday.
"What is zoneless change detection?" The mode where Angular does not rely on zone.js patching browser APIs to know that something happened, and instead runs change detection when it is explicitly notified. Zoneless became stable in Angular v20.2, with improvements to error handling and server-side rendering. The guide then states that zoneless is the default in Angular v21+ so you do not need to do anything to enable it. On v20 you add provideZonelessChangeDetection() at bootstrap and verify that provideZoneChangeDetection is not overriding the default.
The list an interviewer wants back is what still notifies Angular. Change detection is scheduled when:
ChangeDetectorRef.markForCheckis called, whichAsyncPipedoes automaticallyComponentRef.setInputis used- a signal read in a template is updated
- a bound host listener or template listener callback fires
- a view marked dirty by one of the above is attached
Everything else is on you. A setTimeout that mutates a plain class field and is read in a template has nothing on that list, which is why zoneless codebases push state into signals.
"Is OnPush still relevant if we are zoneless?" Yes, and the honest answer separates the two mechanisms. Zoneless changes what schedules a check. OnPush changes which views get checked once one is scheduled. The documented rule for OnPush has not moved: a subtree is checked when the subtree's root receives new inputs as the result of a template binding, or when Angular handles an event (an event binding, an output binding, or @HostListener) in the subtree's root or any of its children. As of v22 you mostly do not write it at all: OnPush is the default for a component that declares no strategy, ChangeDetectionStrategy.Default is deprecated in favour of the new Eager, and ng update writes Eager onto existing components so their behaviour does not move. The modern version of the question is therefore knowing when you need Eager, not remembering to type OnPush — and the discipline OnPush enforces is still the one zoneless requires: never update a binding without a notification.
Building signal, computed and effect from scratch is the fastest way to make this stop being vocabulary, and it is a common JavaScript-round question in its own right (Signals: signal / computed / effect).
RxJS Questions That Still Decide Senior Rounds
Signals did not retire RxJS, and senior rounds still open here.
Observable versus Promise. A Promise is eager, resolves once, and cannot be cancelled. An Observable is lazy (nothing runs until subscription), can emit many values over time, and unsubscribe() tears down the producer. That cancellation property is the whole reason the flattening operators matter.
The four flatteners, answered by use case rather than definition:
switchMapcancels the previous inner subscription when a new value arrives. Typeahead search. This is the one that solves out-of-order responses.mergeMapruns inner subscriptions concurrently and interleaves results. Independent parallel work where order does not matter.concatMapqueues inner subscriptions and runs them strictly in order. A sequence of writes where the second must land after the first.exhaustMapignores new values while an inner subscription is active. A submit button that must not fire twice.
If those blur together, implementing the operator pipeline yourself is the cure (RxJS-Lite Pipeable Operators, Observable).
"How do you avoid subscription leaks?" Three answers, in order of preference. Use AsyncPipe and never subscribe manually. Use takeUntilDestroyed(), whose signature is takeUntilDestroyed<T>(destroyRef?: DestroyRef): called with no argument it injects the current DestroyRef and therefore needs an injection context, and you pass a DestroyRef explicitly to use it elsewhere. Or inject DestroyRef and register teardown with destroyRef.onDestroy(...).
The interop boundary is where modern rounds go next. toSignal creates a signal tracking an Observable, and the subscription it creates automatically unsubscribes when the component or service that called toSignal is destroyed, unless you pass the manualCleanup option. It takes initialValue, or requireSync: true for sources guaranteed to emit synchronously. Going the other way, toObservable uses an effect to track the value of the signal in a ReplaySubject, which is why signals never provide a synchronous notification of changes.
"How do you load data?" The signal-era answer is resource(). It takes a params computation and a loader, and the loader receives an abortSignal you hand to fetch:
query = signal('');
results = resource({
params: () => ({q: this.query()}),
loader: ({params, abortSignal}) =>
fetch(`/api/search?q=${params.q}`, {signal: abortSignal}).then(r => r.json()),
});
If the params computation changes while a load is outstanding, the resource aborts it. That is request cancellation without an operator. The status signal reports 'idle', 'loading', 'reloading', 'resolved', 'error' or 'local', alongside value, hasValue, error, isLoading and a reload() method. Use loader for one-shot async work and stream for continuously updating sources such as WebSockets or server-sent events. httpResource() wraps HttpClient, so it goes through your interceptor stack and exposes request status and response as signals. rxResource lets you define the source as an RxJS Observable and otherwise behaves like resource. resource(), rxResource() and httpResource() are stable as of v22 — the API pages read "stable since v22.0" — so name them as production APIs rather than previews if pressed.
Knowing what a cache-and-dedupe layer does under the hood makes the "when would you write this yourself" follow-up easy (Mini React Query Core, Singleflight).
Templates, Forms, and Routing: The Modern Answers
These are concept-round questions where the syntax changed underneath candidates.
Control flow. @if / @else if / @else, @for with @empty, and @switch. Two details get asked directly. First, track is required on @for: the track expression lets Angular maintain a relationship between your data and the DOM nodes on the page, so it can execute the minimum necessary DOM operations when the data changes. Use $index for static collections and a unique id for dynamic data. Second, @switch has no fallthrough, so there is no equivalent of break.
@for (row of rows(); track row.id) {
<tr><td>{{ row.name }}</td></tr>
} @empty {
<tr><td>No results</td></tr>
}
Note rows() with parentheses. Signal reads in templates are function calls, and forgetting them is a live-coding tell.
Deferrable views. @defer lazy-loads a chunk of template. The triggers are on idle, on viewport, on interaction, on hover, on immediate, on timer, plus the when conditional expression. Sub-blocks are @placeholder (which takes a minimum), @loading (which takes minimum and after), and @error. Prefetch triggers are separated by a semicolon, and hydrate triggers drive incremental hydration on server-rendered pages, including hydrate never to leave content dehydrated indefinitely.
@defer (on interaction; prefetch on idle) {
<comment-thread />
} @placeholder (minimum 500ms) {
<p>Show comments</p>
} @loading (after 100ms; minimum 1s) {
<p>Loading…</p>
} @error {
<p>Could not load comments.</p>
}
minimum on the placeholder is worth explaining unprompted: it prevents a flash when the deferred chunk arrives almost instantly.
Forms. Reactive forms are explicit, typed, and testable, built from FormControl and FormGroup in the class with formControlName in the template. Template-driven forms put the model in the template with ngModel and suit small cases. Signal Forms are stable as of v22, require v21 or later, and build the form from a signal model:
loginModel = signal({email: '', password: ''});
loginForm = form(this.loginModel, (path) => {
required(path.email, {message: 'Email is required'});
email(path.email, {message: 'Enter a valid email address'});
required(path.password, {message: 'Password is required'});
});
Bind with [formField]="loginForm.email", and read state from signals on any node in the field tree: loginForm.email().touched(), .invalid(), .errors().
The "which would you pick" question has a correct answer, and it is not "Signal Forms, they are newer". Angular's own guide says Signal Forms work best in new applications built with signals, and that if you are working with an existing application that uses reactive forms, or if you need production stability guarantees, reactive forms remain a solid choice. Give that answer and you sound like someone who has shipped.
Routing. Lazy-load with loadComponent for a single route and loadChildren for a group. Guards are functions now: a CanActivateFn is a plain function that calls inject() for its dependencies and returns a boolean, a UrlTree, a RedirectCommand, or a Promise or Observable of those.
export const authGuard: CanActivateFn = () => inject(AuthService).isAuthenticated();
Resolvers still prefetch data before activation, though a resource() in the component is often the simpler shape. For reading route state, know that ActivatedRoute exposes params as Observables, and that enabling the withComponentInputBinding router feature binds route parameters straight to component inputs, which pairs neatly with signal inputs.
The Machine-Coding Round: Four Builds and What Is Graded
Four tasks cover most of what gets asked. For each one, the requirement is the easy half. The follow-up, once the happy path works, is what the round is actually testing.
1. Debounced typeahead with request cancellation. Type in a box, hit an API, render suggestions. The follow-up: "the user types fast and stale results overwrite fresh ones, fix it." Graded on: whether you reach for switchMap or a resource() whose params derive from the query signal, since both cancel the in-flight request. debounceTime alone does not solve it, and interviewers know that. Secondary signals: a distinct empty state versus loading state, and whether the input is keyboard-navigable with arrow keys and Escape.
The shape that earns the answer, in about as many lines as you have time for:
export class Typeahead {
private http = inject(HttpClient);
query = signal('');
results = toSignal(
toObservable(this.query).pipe(
debounceTime(250),
distinctUntilChanged(),
switchMap(q => (q ? this.http.get<Item[]>('/api/search', {params: {q}}) : of([]))),
),
{initialValue: [] as Item[]},
);
}
debounceTime cuts the request count and switchMap is the line that fixes the bug: the previous inner subscription is unsubscribed, which aborts the outstanding HttpClient request. The signal-first version of the same build drops the pipe and reads the query signal inside an httpResource request function, which re-issues on change and aborts the previous request for you.
2. Accordion or tabs. Multiple panels, one open at a time. The follow-up: "make it keyboard accessible and add the right ARIA." Graded on: content projection and state ownership. Who owns the open index, the parent or each panel? Can a consumer project arbitrary content into a header? A panel holding its own isOpen boolean makes "only one open" hard, and rewriting it under time pressure is the failure mode. Practise the state-ownership decision on a disclosure and a full accordion before you meet it live.
3. Sortable, paginated table. Click a header to sort, page through results. The follow-up: "now there are 10,000 rows." Graded on: whether sorting and paging are computed derivations of one source array plus sortKey, sortDir and page signals, or imperative mutations of the array you were handed. Mutating the source destroys the original order and shows up immediately when the interviewer asks for a third sort state. The second thing being graded is your @for track expression: track row.id, not track $index, on a list that reorders.
4. Multi-step form with cross-field validation. Three steps, "confirm password must match password", cannot advance while the current step is invalid. The follow-up: "check the username is available on the server." Graded on: validator placement. A cross-field rule belongs on the node that owns both fields, the group, not on either control. An async check belongs in the async validator slot with debouncing, not in a keyup handler. Bonus signal: whether earlier steps keep their state when the user navigates back.
Answers That Are Now Wrong
These cut across all three rounds. Each is an answer that was correct three years ago and now reads as stale.
*ngIf, *ngFor, *ngSwitch. Officially deprecated in v20 in favour of built-in control flow. The NgIf API page reads: "Use the @if block instead. Intent to remove in a future major release." Structural directives in general were not deprecated, only these three. The migration is ng generate @angular/core:control-flow, and after running it CommonModule often no longer needs importing. One caveat to mention if asked: @for view reuse can differ from *ngFor when tracking object properties.
afterRender. Renamed to afterEveryRender in v20. The old name was not kept for backward compatibility and no migration was provided, so a codebase moving to v20 fixes those call sites by hand.
provideExperimentalZonelessChangeDetection. Renamed to provideZonelessChangeDetection in v20, along with provideExperimentalCheckNoChangesForDebug, which became provideCheckNoChangesConfig. That rename also dropped the useNgZoneOnStable option in favour of interval, and the behaviour now applies to every checkNoChanges run.
Writing standalone: true. It has been the default since v19. Writing it by hand is harmless but dates you, and claiming NgModules are deprecated is simply incorrect.
ChangeDetectionStrategy.Default. OnPush is what a component with no declared strategy gets. The API page says Default "is equivalent to setting Eager and is due to be removed", and ng update writes Eager onto existing components so nothing shifts underneath them.
"We use Karma and Jasmine." The Angular CLI uses Vitest as the default unit test runner for new projects. The documented migration schematic is ng g @schematics/angular:refactor-jasmine-vitest, and that migration is considered experimental, which is worth saying rather than glossing.
fixture.detectChanges() everywhere. In a zoneless app, prefer await fixture.whenStable() and let Angular synchronise. Reaching for detectChanges() by reflex is how a test passes while the component under test would never update in production.
If asked "how would you modernise this codebase", naming the schematics is a strong answer: the standalone migration from v19, ng generate @angular/core:control-flow, and the Vitest refactor.
A Two-Week Prep Plan
Days 1 to 4, concept round. Work through the signals and zoneless material until you can recite the five things that notify Angular without hesitating, explain why a computed is lazy and memoized, and state the two documented conditions for checking an OnPush subtree. Then the RxJS four, out loud, by use case. Written answers are easy to nod along with and hard to reproduce under pressure, so say them to an empty room until they are boring.
Days 5 to 10, machine coding. One build per day from the four above, typed from scratch on a timer, then repeated on day 9 or 10 from a blank file. Reading a solution creates the feeling of understanding without the motor memory, and the machine-coding round is entirely motor memory plus one design decision. The value is in a workspace that actually runs, with tests that pass or fail against edge cases rather than an answer you compare yourself against. That is what UIReady's Sandpack plus Jest setup is for, across 660+ questions in React, Vue, Angular and vanilla JS, with progress saved between machines; if you are interviewing on and off for a while, lifetime access costs less than repeating the cycle. Rebuilding the primitives in plain JavaScript pays here too, since the same debounce, event and cancellation questions show up framework-free (the JavaScript coding interview).
Days 11 to 14, code review and rehearsal. Take your own day-5 code and review it as a stranger would. Then rehearse the three questions you cannot bluff:
- What schedules change detection in your current app, and how do you know?
- Where does each subscription in this component end?
- Which of this component's state is owned and which is derived?
Answer those three cleanly and most of the concept round answers itself.