60 questions that really come up in Angular interviews in 2026, split by level: what a junior must know, what is expected from a mid developer who builds features alone, and what a senior is asked about internals, architecture and trade-offs. Each answer starts with what to say first, then what to add to show you understand, the question that usually follows and the answer that fails the interview.
How to use this page
- Answer out loud before you open the answer. Reading feels like knowing; saying it is what you will do in the interview.
- Start with the short answer, then add one or two ideas from “go deeper”. Interviewers prefer a clear position over a long list.
- Mark what you know with “I know this”. It is saved in this browser, and the mock interview gives you the rest first.
- Every answer links to the lesson that explains the topic properly.
Mock interview
A few random questions, one at a time, with the answer hidden until you ask for it. At the end you get the list of questions worth reviewing.
Answer out loud, as if someone were listening. Then reveal the model answer and be honest with yourself.
Junior: the fundamentals
For a first Angular job you are not expected to know internals, but you are expected to know the modern basics without hesitating: standalone components, the new control flow, signals, inputs and outputs, services with inject(), routing and forms. Show that you know why, not only how.
J-01What is a component, and what does “standalone” mean? How does an app start without an NgModule? Components
Say this first
A component is a TypeScript class with
@Componentthat pairs a template with state and behaviour, and is used through its selector. Standalone (the default) means it declares its own dependencies inimports, so no NgModule is needed: the app starts withbootstrapApplication(App, appConfig).Then go deeper
- You no longer write
standalone: true— it is the default. Each component imports exactly what its template uses (other components, pipes,RouterLink), so dependencies are explicit and tree-shakable. - Application-wide setup lives in
app.config.tsasproviders:provideRouter(routes),provideHttpClient(...)only if you configure it, and so on. - An old library that only exports an NgModule can still go directly into a component’s
imports; module providers go throughimportProvidersFrom().
import { bootstrapApplication } from '@angular/platform-browser'; import { App } from './app/app'; import { appConfig } from './app/app.config'; bootstrapApplication(App, appConfig).catch((err) => console.error(err));They’ll likely ask next
Where do you put a provider that the whole application needs?
Red flag
Says every component must be declared in an
AppModule, or importsCommonModule“just in case”.- You no longer write
J-02What kinds of data binding are there in a template? Templates
Say this first
Interpolation
{{ value }}and property binding[prop]="expr"go from component to DOM; event binding(event)="handler($event)"goes from DOM to component; two-way binding[(value)]="sig"combines both.Then go deeper
[disabled]binds a DOM property; for things that exist only as attributes (ARIA,colspan) you use[attr.aria-label].[(x)](“banana in a box”) is sugar for[x]="v()"+(xChange)="v.set($event)". With a signal you pass the signal itself, without parentheses.- Template expressions should be cheap and side-effect free; a signal read like
count()is fine, a heavy method call runs on every check.
<h2>{{ user().name }}</h2> <img [src]="user().avatarUrl" [attr.aria-label]="user().name" /> <button [disabled]="saving()" (click)="save()">Save</button> <input (input)="query.set($any($event.target).value)" /> <app-toggle [(checked)]="notifications" />They’ll likely ask next
What is the difference between an attribute and a property?
Red flag
Thinks
[value]="x"andvalue="x"are the same (the second is a static string), or writes[(checked)]="notifications()".J-03How do you write conditions and loops in a template, and why does
@forneedtrack? TemplatesSay this first
With the built-in control flow:
@if/@else,@forwith@empty, and@switch/@case.trackis mandatory: it tells Angular how to identify each item, so it can reuse DOM nodes instead of recreating the whole list.Then go deeper
- Track by a stable unique id (
track user.id);$indexonly for static lists; tracking the object itself means every refetch destroys and recreates the DOM. - It is part of the language: no
NgIf/NgForimports.@forexposes$index,$first,$last,$even,$odd,$count. @if (user(); as u)gives a typed, non-null alias. Since v21.1 several@casecan share a block, and@default never;(v21.2) makes a forgotten union case a compile error.
@if (tasks().length) { <ul> @for (task of tasks(); track task.id; let i = $index) { <li>{{ i + 1 }}. {{ task.title }}</li> } </ul> } @else { <p>You have no tasks.</p> }They’ll likely ask next
What happens to the DOM if you track by
$indexand then sort the list?Red flag
Still reaches for
*ngIf/*ngForin new code, or cannot explain whattrackis for.- Track by a stable unique id (
J-04What is
@letand when would you use it? TemplatesSay this first
@letdeclares a local, read-only variable in the template, visible in the current block and its children. I use it to name a long expression used several times, or to unwrap a value once, e.g.@let user = user$ | async;.Then go deeper
- It cannot be reassigned from the template; it is recomputed when what it reads changes.
- It is handy inside
@forfor a per-item value (e.g. a final price after discount). - Real logic still belongs in the class as a
computed();@letis for readability, not for business rules.
Red flag
Uses
@letas a mutable variable, or moves complex business logic into the template with it.J-05How do you add a CSS class or style conditionally? Templates
Say this first
With native bindings:
[class.active]="isActive()"for one class,[class]="{ ... }"for several, and[style.width.px]="width()"for styles. They need no import, and static classes are combined with the bound ones, not overwritten.Then go deeper
[class]accepts a string, an array or an object;[style]accepts a string or an object, and units go in the binding (.px,.%,.rem).[ngClass]/[ngStyle]are the old way; v21 has migrationsngclass-to-classandngstyle-to-style.- For the component’s own host element you use the
hostmetadata ('[class.is-loading]': 'loading()'), not@HostBinding.
They’ll likely ask next
How would you set a CSS variable from a signal?
Red flag
Builds class strings by hand in the component, or manipulates
classListdirectly on the element.J-06What is a pipe, and what does “pure” mean? Templates
Say this first
A pipe transforms a value for display directly in the template, like
{{ price | currency }}. By default it is pure: Angular re-runs it only when the input value or its arguments change (by reference), so it is memoised.Then go deeper
- Pipes are standalone and imported like any dependency (
imports: [DatePipe]); a custom one is a class with@Pipeandtransform(). - Because the check is by reference, a pure pipe does not re-run if you mutate an array in place.
- A method called from the template runs on every check; for component-specific calculations prefer
computed(), and keep pipes for reusable display formatting. Avoidpure: false.
They’ll likely ask next
Would you write a pipe or a
computed()to filter a list?Red flag
Makes a filtering pipe impure to “make it work” on a large list.
- Pipes are standalone and imported like any dependency (
J-07How does a parent pass data to a child component, and how does the child talk back? Components
Say this first
The child declares
input()for data coming in andoutput()for events going out. The parent binds[value]="..."and(rate)="onRate($event)"; the child callsrate.emit(4).Then go deeper
- An
input()is a read-only signal: you read it withvalue()and derive from it withcomputed()— nongOnChangesneeded. input.required<T>()makes a missing binding a compile error;transform: numberAttribute/booleanAttributeturnmax="10"or a barereadonlyattribute into the right type.- The child cannot
set()its own input. If it must change the value, it emits an event and lets the parent decide, or usesmodel().
export class StarRating { readonly value = input.required<number>(); readonly max = input(5, { transform: numberAttribute }); readonly rate = output<number>(); protected readonly stars = computed(() => Array.from({ length: this.max() }, (_, i) => i + 1), ); }They’ll likely ask next
What changes if the child needs to modify the value it receives?
Red flag
Says you need
ngOnChangesto react to an input, or uses@Input/EventEmitterin new code.- An
J-08What is
model()and how is it different frominput()+output()? ComponentsSay this first
model()is a writable signal that acts as an input plus an automaticxChangeoutput, so the parent can use two-way binding:[(checked)]="notifications". When the child callsset()orupdate(), the parent’s signal updates.Then go deeper
- Use
model()when the value truly belongs to both sides, as in a toggle, a stepper or a custom input. - Use
input()+output()when the child only proposes and the parent decides (validates, confirms, saves to the server). - In two-way binding you pass the signal itself (
notifications), not its value (notifications()).
Red flag
Cannot explain what
[( )]expands to, or usesmodel()for everything.- Use
J-09What is content projection? Components
Say this first
It lets a parent put its own markup between a component’s tags, and the component decides where to render it with
<ng-content />. Withselectyou can have several slots, for example a title and actions in a card.Then go deeper
selecttakes a tag, attribute or class selector ([card-title]); the<ng-content />withoutselectcatches everything else, and you can put fallback content inside it.- Gotcha: projected content is created by the parent even if the child hides it with
@if. For truly lazy content, pass an<ng-template>. - Projection is for flexible markup (layouts, cards, dialogs); inputs are for data the component formats itself.
They’ll likely ask next
When would you choose projection over an input?
Red flag
Passes HTML as a string input and renders it with
innerHTMLinstead of projecting it.J-10What are
signal()andcomputed()? SignalsSay this first
A
signal()is a reactive value: you read it by calling it, and change it withset()orupdate(). Acomputed()is a read-only value derived from other signals; it is lazy and memoised, recalculated only when its dependencies change.Then go deeper
- Dependencies are tracked automatically: whatever signals the function reads become dependencies, with no subscriptions to manage.
- Signals compare with
===:items().push(x)notifies nobody; useupdate((list) => [...list, x]). - Rule of thumb: anything that can be calculated from other state is a
computed(), not a second signal kept in sync. Expose state withasReadonly().
export class Counter { protected readonly count = signal(0); protected readonly double = computed(() => this.count() * 2); protected increment() { this.count.update((c) => c + 1); } } // template: {{ count() }} · {{ double() }}They’ll likely ask next
Why doesn’t the UI update after
items().push(x)?Red flag
Tries to call
set()on acomputed(), or forgets the parentheses and binds the signal function instead of its value.J-11What is
effect(), and when should you not use it? SignalsSay this first
effect()runs a function once, then again whenever the signals it reads change. It is for side effects towards the outside world —localStorage, logging, a third-party library — not for computing state.Then go deeper
- Do not use an effect to copy one signal into another: that is a
computed()(orlinkedSignal()if it must also be editable). - Do not fetch data from an effect: there is no cancellation and old responses can win the race. Use
httpResource()/resource(). - It must be created in an injection context (constructor or field), is destroyed with the component, and
onCleanupclears timers before the next run.
They’ll likely ask next
What happens if you create an effect inside
ngOnInit?Red flag
Uses
effect()to keepfullNamein sync withfirstNameandlastName.- Do not use an effect to copy one signal into another: that is a
J-12What is the difference between the constructor and
ngOnInit? How do you clean up when a component is destroyed? ComponentsSay this first
The constructor runs when the class is created and is the injection context:
inject(), signals and effects go there or in field initialisers.ngOnInitruns once after the first inputs are set. For cleanup I useinject(DestroyRef).onDestroy()ortakeUntilDestroyed().Then go deeper
- Reading an
input.required()in the constructor throws, because the parent has not bound it yet. With signal inputs you rarely needngOnInit: acomputed()orhttpResource()reads the input when it is ready. inject()does not work inngOnInitor in a click handler — that is outside the injection context (error NG0203).DestroyRefalso works in services, directives and reusable functions, whichngOnDestroycannot do. For DOM access after render, useafterNextRender().
export class Clock { readonly now = signal(new Date()); constructor() { const id = setInterval(() => this.now.set(new Date()), 1000); inject(DestroyRef).onDestroy(() => clearInterval(id)); } }They’ll likely ask next
Where would you initialise a chart library that needs the DOM element?
Red flag
Says the constructor is “only for DI” and puts all logic in
ngOnInit, or never cleans up intervals and subscriptions.- Reading an
J-13What is a service and how do you use it in a component? DI
Say this first
A service is a class that holds logic or state shared between components — API calls, a cart, the logged-in user. In v22 I mark it with
@Service(), which is provided in root as a singleton, and get it withinject(CartService).Then go deeper
@Service()is equivalent to@Injectable({ providedIn: 'root' }): created on first injection and tree-shakable. It requiresinject()— constructor DI is a compile error.@Injectableis still valid (existing code, libraries for older versions). Putting the service in a component’sprovidersgives one instance per component instead of one per app.inject()also works in functional guards, resolvers and interceptors, and avoids longsuper(...)calls with inheritance.
@Service() export class CartService { private readonly _items = signal<CartItem[]>([]); readonly items = this._items.asReadonly(); readonly count = computed(() => this._items().length); add(item: CartItem) { this._items.update((list) => [...list, item]); } }They’ll likely ask next
How would you give each instance of a wizard component its own copy of a service?
Red flag
Creates the service with
new CartService(), or thinks a service inprovidersis still the same global singleton.J-14How do you set up routing and navigate between pages? Routing
Say this first
You define a
Routesarray, register it withprovideRouter(routes), and place<router-outlet />where pages render. You navigate withrouterLinkin templates androuter.navigate()in code; pages are usually lazy-loaded withloadComponent.Then go deeper
loadComponent: () => import('./home-page')puts the page in a separate chunk, downloaded only when the route is visited;loadChildrendoes the same for a whole feature.- Order matters — the first match wins — so the
'**'wildcard goes last.routerLinkActiveadds a class to the active link. - Use
routerLink, nothref:hrefreloads the whole application.
They’ll likely ask next
How would you protect a route so only logged-in users can open it?
Red flag
Navigates with
<a href>orwindow.location, or puts the wildcard route first.J-15How does a page read a route parameter like
:id? RoutingSay this first
With
withComponentInputBinding()inprovideRouter, path params, query params and route data arrive as component inputs:readonly id = input.required({ transform: numberAttribute }). Because it is a signal, anything derived from it updates when the id changes.Then go deeper
- Gotcha: navigating from
/products/1to/products/2reuses the same component instance; code that reads the id once (e.g. a snapshot inngOnInit) shows stale data. - Params are strings in the URL, so a transform like
numberAttributegives you a real number. - Since v22 child routes inherit params and data from all parents by default (
paramsInheritanceStrategy: 'always').
@Component({ templateUrl: './product-page.html' }) export default class ProductPage { readonly id = input.required({ transform: numberAttribute }); // :id readonly tab = input<'specs' | 'reviews'>('specs'); // ?tab= readonly product = httpResource<Product>(() => '/api/products/' + this.id()); }They’ll likely ask next
What breaks if you read the id only once with
route.snapshot?Red flag
Only knows
route.snapshot.paramMap.get('id')and does not know the component can be reused.- Gotcha: navigating from
J-16How do you call a REST API in Angular? HTTP
Say this first
With
HttpClient, injected in a service:http.get<Product[]>('/api/products')returns an Observable. Since v21 it is available in root with no setup;provideHttpClient()is only for configuration such as interceptors.Then go deeper
- The Observable is cold: no request is sent until someone subscribes (
subscribe(),firstValueFrom(), the async pipe). - For reactive reads in a component,
httpResource()givesvalue(),isLoading()anderror()as signals and cancels old requests; for writes (POST, PUT) you callHttpClient. - The generic
get<T>is only a type assertion, not validation. In v22 Fetch is the default backend; cross-cutting concerns (auth token, errors) go in functional interceptors.
They’ll likely ask next
Where would you add an
Authorizationheader for every request?Red flag
Calls
http.get()without subscribing and wonders why nothing happens, or callsfetchdirectly from components.- The Observable is cold: no request is sent until someone subscribes (
J-17How do you build a form with validation in Angular 22? Forms
Say this first
For new forms I use Signal Forms, stable in v22: a signal holds the model,
form(model, schema)builds a tree of fields with rules likerequired(p.email), and[formField]binds each input. Reactive Forms remain the choice for existing apps full of them, or libraries that only supportControlValueAccessor.Then go deeper
- The model signal is the source of truth — no internal copy, no
patchValue:model.set(data)fills the form, and the form writes straight into the model. - Each field’s state is signals:
f.email().invalid(),touched(),errors(); the whole form is a field too (f().valid(),f().submitting()). - Show errors only after
touched(), and always validate on the server too — client validation is UX, not security.
export class LoginForm { readonly model = signal({ email: '', password: '' }); readonly loginForm = form(this.model, (p) => { required(p.email); email(p.email); minLength(p.password, 8); }); } // template: <input [formField]="loginForm.email" />They’ll likely ask next
How would you validate that two password fields match?
Red flag
Validates by hand in the submit handler with
ifs, or says Signal Forms are still only experimental in v22.- The model signal is the source of truth — no internal copy, no
J-18What does
@deferdo? PerformanceSay this first
@deferlazy-loads a part of a template: the components inside it go into a separate chunk, downloaded only when a trigger fires —on idleby default, oron viewport,on interaction,when cond(). It reduces the initial bundle without touching the routes.Then go deeper
@placeholderis shown before the trigger (needed foron viewport/interaction/hover),@loadingwhile downloading,@errorif the download fails.- Gotcha: if a deferred component is also referenced elsewhere in the same file (outside the block), it is loaded eagerly and the chunk is lost.
prefetch on hoverdownloads early and renders later — good for actions “one click away”.
@defer (on viewport) { <app-comments [postId]="post().id" /> } @placeholder { <div class="comments-skeleton"></div> } @loading (after 100ms) { <app-spinner /> }They’ll likely ask next
What is the difference between
@deferand@if?Red flag
Thinks
@deferis just a delayed@ifand does not mention the separate JavaScript chunk.J-19How does Angular know when to update the screen? What changed in v21 and v22? Change detection
Say this first
Change detection re-renders templates when something notifies Angular: a signal read in the template changes, a template event fires, the async pipe emits, or
markForCheck()is called. Since v21 new apps are zoneless (no zone.js), and since v22 components areOnPushby default.Then go deeper
- Without zone.js, a
setTimeoutorsubscribe()that changes a plain field does not update the UI. The fix is a signal (ortoSignal()/ the async pipe). - An
OnPushcomponent is checked only when an input gets a new reference, an event happens in it, a signal it reads changes, or it is marked for check — otherwise its subtree is skipped. ChangeDetectionStrategy.Eageris the old “always check” behaviour (Defaultis deprecated);ng updateadds it where needed.
They’ll likely ask next
A value set inside
setTimeoutdoesn’t appear on screen. Why, and how do you fix it?Red flag
Says “zone.js detects every change automatically” for a v21+ app, or fixes missing updates by sprinkling
detectChanges().- Without zone.js, a
J-20How do you test a component in a new Angular project? Testing
Say this first
Since v21
ng testruns Vitest (Karma is deprecated). I create the component withTestBed.createComponent(), set inputs,await fixture.whenStable()so the zoneless render finishes, then assert onfixture.nativeElement.Then go deeper
- Test behaviour the user sees (text, clicks, emitted outputs), not private implementation details.
- Inputs can be passed as signals with
inputBinding(); outputs are spied withoutputBinding()andvi.fn(). - For HTTP, use
provideHttpClientTesting()andHttpTestingControllerinstead of a real server; services and signal stores can be tested withTestBed.inject()and no DOM.
it('shows the right number of stars', async () => { const fixture = TestBed.createComponent(StarRating, { bindings: [inputBinding('value', () => 3)], }); await fixture.whenStable(); const stars = fixture.nativeElement.querySelectorAll('button'); expect(stars).toHaveLength(5); });They’ll likely ask next
How would you test a component that loads data over HTTP?
Red flag
Has never written a test, or relies on
fakeAsync/tickfrom zone.js in a zoneless Vitest project.
Mid: building features on your own
At this level the questions are about choices: computed or effect, which RxJS operator, where a provider lives, how a component is split, how you test it. A good mid answer names the option, the reason, and the case where the other option wins.
M-01When do you use
computed()and wheneffect()? Why shouldn’t you set signals inside an effect? SignalsSay this first
computed()is for derived state: a pure, lazy, memoized value.effect()is for side effects that push signal values to the non-reactive world (DOM,localStorage, logging). Signal → signal through an effect is almost always acomputed()orlinkedSignal()in disguise.Then go deeper
- Setting a signal in an effect means an extra run, a moment where the target is stale (one render with a wrong value) and a dependency graph that’s hard to follow.
- Effects are not synchronous: set a signal three times and the effect runs once, with the final value. Component effects run during change detection; effects in root services run as microtasks.
- Dependencies are dynamic: only signals read in the last run are tracked. Use
untracked()for reads that shouldn’t re-trigger the effect. - An effect needs an injection context (constructor or field initializer) or an explicit
injector; otherwise you get NG0203. UseonCleanupfor timers and listeners. - HTTP from an effect is a smell: no cancellation, so responses can arrive out of order. That’s what
resource/httpResourceare for.
readonly firstName = signal('Ana'); readonly lastName = signal('Pop'); // ✗ effect used to synchronise state // effect(() => this.fullName.set(this.firstName() + ' ' + this.lastName())); // ✓ derived state readonly fullName = computed(() => this.firstName() + ' ' + this.lastName()); // ✓ effect: reactive world → outside world constructor() { effect(() => localStorage.setItem('name', this.fullName())); }They’ll likely ask next
What if the derived value must also be editable by the user?
Red flag
“I use effect to keep two signals in sync” or “computed and effect are basically the same, effect just runs more often”.
M-02What problem does
linkedSignal()solve? Give a real example. SignalsSay this first
It’s a writable signal that resets from a source: derived like
computed(), but you can alsoset()it. Typical cases: a selection that defaults to the first option of a list, a page number that goes back to 1 when filters change, an editable copy of an input.Then go deeper
- It replaces the fragile signal + effect pattern: no extra run, no render with a stale value.
- The long form (
source+computation) gives you thepreviousvalue, so you can keep the user’s choice if it still exists in the new list. - A manual
set()is valid only until the source changes again; then the computation wins. - Since v22.1 the
setoption intercepts writes, e.g. to clamp a quantity to the available stock.
They’ll likely ask next
How would you build an editable draft of an
input()with a “dirty” flag?Red flag
Copying the input into a signal in
ngOnInit(it never resets) or syncing with an effect.M-03How do
resource/httpResourcework, and why are they better than subscribing to HTTP inside an effect? HTTPSay this first
A resource turns async loading into signals: a reactive
params(or URL) function, and when it changes the old request is cancelled and a new one starts. You getvalue(),status(),isLoading(),error()andreload()for free, with no race conditions.Then go deeper
resourcegets anabortSignalin the loader;httpResourcegoes throughHttpClient, so interceptors apply;rxResourcetakes an Observable stream and behaves likeswitchMap.- Returning
undefinedfrom params means statusidle: the loader doesn’t run. In v22 you can also throwResourceParamsStatus.IDLE/LOADING, and chain resources withctx.chain(). hasValue()is a type guard;defaultValueremovesundefined;parsevalidates or maps the response (e.g. DTO → model, Zod).- Resources are for reading. Writes (POST/PUT/DELETE) go through
HttpClient, then youupdate()the resource locally (optimistic) orreload()it.
readonly userId = input.required<number>(); // a new id → the old request is cancelled, a new one starts readonly user = httpResource<User>(() => '/api/users/' + this.userId()); // template: // @if (user.isLoading()) { <app-spinner /> } // @if (user.error()) { <button (click)="user.reload()">Retry</button> } // @else if (user.hasValue()) { <h2>{{ user.value().name }}</h2> }They’ll likely ask next
How would you delete an item from a list loaded with
httpResourceand handle a failure?Red flag
Using
httpResourcefor a POST on button click, or not knowing what happens to an in-flight request when the params change.M-04How do you implement a search-as-you-type without firing a request on every keystroke? Signals
Say this first
In v22 I wrap the query signal with
debounced(query, 300)and feed itsvalue()into anhttpResource. The debounce removes the noise; the resource cancels stale requests, so the last query always wins.Then go deeper
debounced()returns aResource:value()updates after the last change, andisLoading()is true while the user is still typing.- It’s experimental in v22. The classic alternative is
toSignal(toObservable(query).pipe(debounceTime(300), distinctUntilChanged())): three conversions for one debounce. waitcan be a function, e.g. no delay when the field is cleared.- In Signal Forms there’s a dedicated
debounce(path.field, 300)rule, andvalidateHttphas its owndebounceoption.
import { httpResource } from '@angular/common/http'; import { debounced, signal } from '@angular/core'; export class UserSearch { readonly query = signal(''); readonly search = debounced(this.query, 300); readonly users = httpResource<UserDto[]>( () => '/api/users?q=' + encodeURIComponent(this.search.value()), { defaultValue: [] }, ); }They’ll likely ask next
Debounce alone doesn’t prevent out-of-order responses. What does?
Red flag
A
setTimeoutin the input handler with a manualsubscribe()and no cancellation of the previous request.M-05How do you combine signals and RxJS in a component without leaking subscriptions? When do you keep RxJS? RxJS
Say this first
Signals for the displayed state, RxJS at the edges (events, streams, complex async).
toSignal()subscribes and unsubscribes automatically; for a manualsubscribe()I addtakeUntilDestroyed(), which ties it to the component’sDestroyRef.Then go deeper
toSignal()withoutinitialValueis typedT | undefined;requireSync: trueworks only if the source emits synchronously (e.g. aBehaviorSubject).toObservable()emits asynchronously (through an effect) and only the last value if the signal is set several times in the same tick — it’s not an event stream.takeUntilDestroyed()without arguments works only in an injection context; elsewhere passinject(DestroyRef)explicitly. For non-RxJS cleanup useDestroyRef.onDestroy().- Keep RxJS for websockets, combining event streams, retry/backoff, and flattening policies (
switchMap,exhaustMap…). Signals have no notion of time.
They’ll likely ask next
Why can’t you call
toSignal()inside a click handler?Red flag
“Signals replace RxJS completely” — or the opposite: subscribing everywhere and copying values into fields with no unsubscription.
M-06Explain
switchMapvsmergeMapvsconcatMapvsexhaustMap, with a use case for each. RxJSSay this first
They differ in what happens when a new value arrives while the previous inner Observable is still running:
switchMapcancels it (search),mergeMapruns in parallel (uploads),concatMapqueues in order (ordered saves),exhaustMapignores the new one (submit, login).Then go deeper
- Choosing the operator is choosing a concurrency policy; the wrong one causes real bugs: double orders with
mergeMapon a pay button, lost saves withswitchMapon autosave. mergeMapaccepts a concurrency limit, e.g.mergeMap(upload, 3).- Put
catchErrorinside the inner pipe: after the flattening operator, the first error completes the outer stream and the search “dies”. - With signals,
httpResourceandrxResourcealready give youswitchMapsemantics for reads.
// the last one wins; errors handled INSIDE, so the stream survives query$.pipe( debounceTime(300), switchMap((q) => this.api.search(q).pipe(catchError(() => of([])))), ); files$.pipe(mergeMap((file) => this.api.upload(file), 3)); // parallel, max 3 patches$.pipe(concatMap((patch) => this.api.save(patch))); // in order submit$.pipe(exhaustMap(() => this.api.login(this.credentials()))); // ignore double clicksThey’ll likely ask next
Which one would you use for autosave of a document, and why not
switchMap?Red flag
“I always use
switchMap” or nestedsubscribe()calls instead of a flattening operator.- Choosing the operator is choosing a concurrency policy; the wrong one causes real bugs: double orders with
M-07In a zoneless Angular 22 app, what triggers change detection, and why might a field updated in
setTimeoutnot show up? Change detectionSay this first
Without zone.js nothing patches timers or promises. Change detection is scheduled by a signal read in the template changing, a template/host event, the
asyncpipe,markForCheck(),setInput()or a navigation. A plain field set insetTimeoutorsubscribe()notifies nobody — make it a signal.Then go deeper
- Since v22 components without
changeDetectionare OnPush; the old behaviour isChangeDetectionStrategy.Eager(Defaultis deprecated).ng updateadds Eager where needed. - An OnPush component is checked only on a new input reference (
===), an event in it or its children, a signal read in its template, theasyncpipe, ormarkForCheck(). Otherwise the whole subtree is skipped. - Mutating an array passed as input (
push) doesn’t change the reference: use immutable updates. markForCheck()is the emergency fix for legacy callbacks;detectChanges()is synchronous and rarely needed.
// ✗ works with zone.js, NOT zoneless + OnPush // now = new Date(); // setInterval(() => (this.now = new Date()), 1000); // ✓ a signal schedules change detection by itself export class Clock { readonly now = signal(new Date()); constructor() { const id = setInterval(() => this.now.set(new Date()), 1000); inject(DestroyRef).onDestroy(() => clearInterval(id)); } }They’ll likely ask next
How would you migrate an older component that relies on Zone.js to OnPush + zoneless, step by step?
Red flag
Calling
detectChanges()everywhere, or believing OnPush means “the component never updates unless inputs change”.- Since v22 components without
M-08How do
viewChild()andcontentChildren()differ from the old decorators, and when can you safely read them? ComponentsSay this first
Signal queries return signals, so results are reactive: you derive from them with
computed()instead ofngAfterViewInitandQueryList.changes. The view doesn’t exist in the constructor, so DOM work goes inafterNextRender();viewChild.required()throws if read too early.Then go deeper
viewChildlooks in the component’s own template;contentChildlooks in what the parent projected (e.g. a Tabs container discovering its Tab children).contentChildrenlooks only at direct children by default; usedescendants: truefor deeper matches.readpicks what you get:ElementRef,TemplateRef,ViewContainerRef, a directive, or (v22.2) the child’sInjector.- Gotcha: querying a component by type in the class makes it eager, so it won’t be split out by a
@deferblock.
They’ll likely ask next
How would a Tabs component render only the active tab’s content?
Red flag
Reading a view query in the constructor or
ngOnInitand being surprised it’sundefined.M-09Content projection with
ng-contentvs passing anng-templaterendered withngTemplateOutlet: when do you choose which? TemplatesSay this first
ng-contentis for static slots: the parent creates the content once, whether or not the child shows it. Anng-templateis a recipe the child instantiates when and as often as it wants, with a context — needed for lazy content and for “how should a row look” APIs.Then go deeper
- Wrapping
ng-contentin@ifhides the DOM, but the projected components already exist and their lifecycle already ran. - Named slots use
select(tag, attribute, class);ngProjectAsprojects several elements into one slot without a wrapper. - The context passes data back:
$implicitmaps tolet-item. Without a directive withngTemplateContextGuard, thelet-variables areany. - A template sees the injector where it was declared; since v21.2
[ngTemplateOutletInjector]="'outlet'"makes it use the injector where it’s rendered.
@Component({ selector: 'app-collapsible', imports: [NgTemplateOutlet], template: ` <button (click)="open.set(!open())">Toggle</button> @if (open()) { <ng-container [ngTemplateOutlet]="content() ?? null" /> } `, }) export class Collapsible { readonly content = contentChild(TemplateRef); // created only when opened readonly open = signal(false); } // <app-collapsible><ng-template><app-heavy-report /></ng-template></app-collapsible>They’ll likely ask next
How do you make the
let-variables of a template strongly typed?Red flag
Thinking
@ifaroundng-contentmakes the projected content lazy.- Wrapping
M-10How would you build a reusable, type-safe data table component without turning it into a “god component”? Components
Say this first
Make the component generic (
DataTable<T>):Tis inferred fromrows, column keys are restricted tokeyof T, outputs are typed. Customisation goes through cell templates, not boolean inputs, and domain-specific wrappers live in the feature.Then go deeper
- The god component smell:
any[]data,isProductTableflags, 30 inputs and domain knowledge inshared/. - Cell templates are found with
contentChildren()on a directive;ngTemplateContextGuardtypeslet-rowasTinstead ofany. - Pass behaviour as functions (
trackBy,displayWith,compareWith) so the component doesn’t need to know the shape of the data. - A typo in a column key becomes a compile error, not an empty column in production.
They’ll likely ask next
Where would the users-specific table live, and what would it add on top of the generic one?
Red flag
Adding another boolean input every time a page needs something different.
- The god component smell:
M-11How do you render a component chosen at runtime, e.g. dashboard widgets from a config? Components
Say this first
Climb a ladder: a small known set →
@switch; the component is a value →NgComponentOutletwithinputs; full control (outputs, directives) →ViewContainerRef.createComponent()withbindings(inputBinding,outputBinding,twoWayBinding).Then go deeper
- A registry of
() => import(…)loaders makes each widget a separate lazy chunk. inputBinding('title', () => …)is reactive, unlike the oldsetInput()that you had to call again on every change, plus manual output subscriptions.ComponentFactoryResolverwas removed in v22: you pass the class directly tocreateComponent.- For toasts and dialogs outside the tree, use the standalone
createComponent()with anEnvironmentInjector, thenappRef.attachView()so it joins change detection, and destroy it yourself.
private readonly vcr = inject(ViewContainerRef); private async render(type: WidgetType) { const component = await WIDGET_REGISTRY[type](); // lazy chunk this.vcr.clear(); this.vcr.createComponent(component, { bindings: [ inputBinding('title', () => this.config().title), // stays in sync outputBinding('remove', () => this.removed.emit(this.config().id)), ], }); }They’ll likely ask next
How do you make sure all widgets accept the same inputs and outputs?
Red flag
Reaching for
ComponentFactoryResolverorinnerHTMLwith component tags.- A registry of
M-12How do you manage the state of a feature page without a state library? Where do smart and dumb components fit in? Architecture
Say this first
A signal-based store service: private writable signals, public read-only signals (
asReadonly()) andcomputed()selectors, plus methods with domain names doing immutable updates. The routed (smart) page provides and injects the store; dumb components get data through inputs and emit intentions through outputs.Then go deeper
- Scope by lifetime: feature state in
providersof the page or route (dies with it), global state (user, cart, theme) in a root@Service(). - Server data goes into
httpResourceinside the store; state that must survive a refresh or be shareable goes into the URL (query params +input()). - A dumb component may have local UI state and inject UI things (
ElementRef,DestroyRef), but never data services or the router — otherwise it can’t be reused or tested easily, and a list of 50 cards makes 50 requests. - NgRx SignalStore becomes worth it when the team wants conventions, plugins and devtools, not by default.
They’ll likely ask next
Why expose read-only signals instead of the writable ones?
Red flag
Putting every piece of state in one global root service, or exposing writable signals that any component mutates in place.
- Scope by lifetime: feature state in
M-13Explain the DI hierarchy: when do you provide in root, on a route or on a component, and when do you need an
InjectionToken? DISay this first
inject()looks up the element injectors (current element, then DOM parents), then the route injector, root and platform, and throws NG0201 if nothing matches. Root (@Service()) means one app-wide singleton; routeprovidersmean one instance per feature; componentprovidersmean one instance per component and its children. Tokens are for values that aren’t classes: config, URLs, functions.Then go deeper
@Service()(v22) equalsprovidedIn: 'root', is tree-shakable and allows onlyinject().autoProvided: falseforces you to list it inproviders.inject()beats constructor DI: nosuper(…)chains, it works in guards, interceptors and factories, and enables reusableinjectX()functions. It needs an injection context (NG0203 otherwise).useClass,useValue,useFactory,useExistingandmulti: truefor plugin-style arrays;optional,self,skipSelf,hostmodify the lookup.viewProvidersare invisible to projected content;EnvironmentProviders(provideX()) can go only at app or route level, not in a component.
They’ll likely ask next
What happens to a service provided on a lazy route when the user navigates away?
Red flag
Listing a root service in a component’s
providers“to be safe” — creating a second instance and hard-to-find state bugs.M-14Functional guards and resolvers: what’s the difference between
canMatchandcanActivate, and when would you avoid a resolver? RoutingSay this first
Guards are plain functions using
inject().canMatchdecides whether the route “exists” for the user — if not, the router tries the next route and the lazy chunk isn’t downloaded.canActivateblocks entry to a route that matched. A resolver delays navigation until data arrives; for lists and dashboards I preferhttpResourcein the page.Then go deeper
- Return
true/false, aUrlTree, or aRedirectCommand(redirect + navigation options likereplaceUrl); since v22.2 you can also throw aRedirectCommand. - Resolver: the page appears with data ready, but navigation waits and an error cancels it.
httpResource: instant navigation, loading and error are UI state. - With
withComponentInputBinding()resolved data, params and query params arrive asinput()s. - Guards are UX, not security: the API must enforce authorization too.
canDeactivateprotects unsaved changes.
They’ll likely ask next
How would you show a different dashboard for admins and regular users on the same path?
Red flag
Class-based guards with constructor injection as the default, or treating a guard as the only access control.
- Return
M-15Write a functional HTTP interceptor that adds an auth token. What are the gotchas? HTTP
Say this first
An
HttpInterceptorFnis a function(req, next)that returnsnext(req)— or a clone of the request, because requests are immutable. It’s registered withprovideHttpClient(withInterceptors([…]))and can useinject().Then go deeper
- Only attach the token to your own API (check the URL), never to third-party hosts.
- Order matters: requests go through the interceptors in the listed order, responses come back in reverse.
- Call
inject()at the top of the function, not inside RxJS callbacks likecatchErrorthat run later, outside the injection context. - Use
HttpContextTokenfor per-request metadata (e.g. skip the loading bar) instead of fake headers.httpResourceandvalidateHttpalso go through interceptors.
export const authInterceptor: HttpInterceptorFn = (req, next) => { const token = inject(AuthStore).token(); if (!token || !req.url.startsWith('/api')) return next(req); return next(req.clone({ setHeaders: { Authorization: 'Bearer ' + token } })); }; // app.config.ts provideHttpClient(withInterceptors([authInterceptor, errorInterceptor]));They’ll likely ask next
How would you handle a 401 by refreshing the token once and retrying the original request?
Red flag
Mutating
req.headersdirectly, or sending the token to every URL.M-16How do you structure error handling in an Angular app: HTTP errors, rendering errors and everything else? Architecture
Say this first
In layers: predictable HTTP errors (404, 422) are UI state in the store or component (
resource.error()); cross-cutting ones (401, 5xx) go in an interceptor; a broken area of the page is isolated with@boundary; anything uncaught ends up in a custom globalErrorHandlerfor logging and telemetry.Then go deeper
@boundary(v22.2, developer preview) has@error (let e = $error; let reset = $reset)blocks, optionally withwhenconditions — the first match wins.@boundarycatches rendering errors only, not errors from event handlers or async code — those still reach theErrorHandler.provideBrowserGlobalErrorListeners()forwardswindow.onerrorand unhandled rejections to theErrorHandler.- An interceptor should rethrow (
throwError(() => err)) after its cross-cutting reaction, so the caller can still react. Validation errors are not exceptions: they live in the form’serrors().
@boundary { <app-price-widget [price]="price()" /> <app-recommendations /> } @error (let e = $error; when e instanceof PermissionError) { <p>You don't have access to this widget.</p> } @error (let e = $error; let retry = $reset) { <app-error-card [message]="e.message" (retry)="retry()" /> }They’ll likely ask next
Where does an error thrown in a click handler end up?
Red flag
A
try/catchwithconsole.login every component, or swallowing errors in an interceptor so nobody else sees them.M-17How does validation work in Signal Forms, including async validation, and how is it different from typed Reactive Forms? Forms
Say this first
In Signal Forms the model is a plain signal, and
form(model, schema)builds a field tree over it with rules declared in a schema function (required,email,validate,validateHttp…). Every field state is a signal:valid(),errors(),pending(),touched(). Reactive Forms keep their own copy of the value insideFormControls and expose changes as Observables.Then go deeper
- No internal copy:
model.set()updates the inputs and typing updates the model — nopatchValue/valueChanges. - Cross-field rules use
valueOf()invalidate; conditional ones usewhen,hidden,disabled(hidden and disabled fields aren’t validated); reusable rules useschema()+apply(). validateHttpgoes throughHttpClient(so interceptors apply) and has adebounceoption in v22;validateAsyncworks with any resource.pending()is true while it runs.- Server errors from
submission.actioncan be returned with afieldTreeso they land on the right field. - Migration can be gradual with
SignalFormControlandcompatForm; typed Reactive Forms (nonNullable,getRawValue()) remain valid for existing code.
readonly model = signal({ username: '', password: '' }); readonly signup = form(this.model, (p) => { required(p.username); minLength(p.password, 8, { message: 'At least 8 characters' }); validateHttp(p.username, { request: ({ value }) => '/api/username-available?username=' + value(), debounce: 400, onSuccess: (res: { available: boolean }) => res.available ? undefined : { kind: 'taken', message: 'Username taken' }, onError: () => ({ kind: 'network', message: 'Could not verify' }), }); });They’ll likely ask next
How would you validate that the end date is after the start date?
Red flag
Writing an async validator that fires a request on every keystroke without debounce, or not knowing the field is
pendingmeanwhile.- No internal copy:
M-18How do you build a custom form control (e.g. a star rating) that works with Signal Forms? And with Reactive Forms? Forms
Say this first
For Signal Forms you implement
FormValueControl<T>: the only required member isvalue = model(); then[formField]works on it exactly like on aninput. For Reactive Forms the classic way isControlValueAccessorregistered withNG_VALUE_ACCESSOR.Then go deeper
- Optional inputs like
disabled,invalid,touchedare filled in automatically by the form — declare only what you use. In v22 atouchoutput reports that the user touched the control. - Boolean controls implement
FormCheckboxControlwithcheckedinstead ofvalue. - A CVA needs four methods (
writeValue,registerOnChange,registerOnTouched,setDisabledState) and you must callonChangeyourself; forgettingonTouchedbreaks “show errors after touch”. - Interop in v22: Signal Forms can bind CVAs, and template/reactive forms support
FormValueControl, so one control can serve both during migration.
They’ll likely ask next
How does the form disable your control, and do you need to handle that yourself?
Red flag
Passing a
FormControlas an input to the child component and mutating it there.- Optional inputs like
M-19How does
@deferwork, which triggers would you use, and what makes a deferred block not actually lazy? PerformanceSay this first
@defermoves the block’s components into a separate chunk loaded on a trigger:on idle(default),viewport,interaction,hover,timer,immediateorwhena condition.prefetchdownloads earlier than it renders. It only stays lazy if those dependencies are standalone and used nowhere else in the file.Then go deeper
- Referencing a deferred component in the class (e.g.
viewChild(HeavyChart)) or elsewhere in the template makes it eager. @placeholdershows before the trigger (and is the observed element forviewport/interaction/hoverwithout a target);@loading (after 100ms; minimum 500ms)avoids flicker;@errorhandles a failed chunk download.whenfires only once; several triggers are combined with OR.- New:
on viewport({ rootMargin: '200px' })(v21),on idle(500)(v22). With SSR,hydrate on …triggers give incremental hydration, enabled by default in v22.
@defer (on viewport; prefetch on idle) { <app-comments [postId]="post().id" /> } @placeholder (minimum 300ms) { <div class="comments-skeleton"></div> } @loading (after 100ms; minimum 500ms) { <app-spinner /> } @error { <p>We couldn't load the comments.</p> } @defer (on interaction; prefetch on hover) { <app-share-dialog /> } @placeholder { <button>Share</button> }They’ll likely ask next
How would you check that the block really ended up in a separate chunk?
Red flag
Wrapping above-the-fold content in
@defer, or thinking@deferdelays an HTTP request rather than loading code.- Referencing a deferred component in the class (e.g.
M-20How do you test a component with signal inputs and a service that calls HTTP, using Vitest in Angular 21+? Testing
Say this first
Components:
TestBed.createComponent(Cmp, { bindings: [inputBinding('value', signal)] }), thenawait fixture.whenStable(), since there’s no zone to flush. HTTP:provideHttpClient()+provideHttpClientTesting(), thenHttpTestingController.expectOne(url).flush(data)andverify()inafterEach.Then go deeper
- Vitest is the default runner since v21 (
@angular/build:unit-test); Karma is deprecated. Usevi.fn()for output spies andvi.useFakeTimers()instead offakeAsync. - Changing the bound signal updates the input like in a real parent — no
setInput()needed. - Signal stores are the easiest to test:
TestBed.inject(), call methods, assert oncomputed()s. Effects run asynchronously, so flush them withTestBed.tick(). - Test behaviour through the DOM or harnesses, not private fields.
TestBed.createDirective()(v22.2) tests a directive without a hand-written host.
beforeEach(() => { TestBed.configureTestingModule({ providers: [provideHttpClient(), provideHttpClientTesting()] }); api = TestBed.inject(ProductsApi); http = TestBed.inject(HttpTestingController); }); afterEach(() => http.verify()); // no unexpected requests it('maps DTO → Product', async () => { const result = firstValueFrom(api.get(1)); http.expectOne('/api/products/1').flush({ id: 1, name: 'Mouse', price: 99, stock: 0 }); expect(await result).toEqual({ id: 1, name: 'Mouse', price: 99, inStock: false }); });They’ll likely ask next
How would you test a component that uses
httpResource?Red flag
Hitting the real backend in unit tests, or calling
detectChanges()repeatedly and asserting on private fields.- Vitest is the default runner since v21 (
Senior: internals, architecture, trade-offs
Senior interviews check whether you can decide for a team: how the framework works underneath, how an app stays maintainable at 50 developers, what SSR, security and performance cost. Always answer with a position, the trade-off, and how you would introduce it in a real project.
S-01How do signals work internally? Explain the producer/consumer graph, push-pull and why they are glitch-free. Signals
Say this first
Signals form a reactive graph built from reads:
signal()is a producer,computed()is both, effects and templates are live consumers. A write only bumps a version and pushes a “maybe dirty” notification; values are pulled and recomputed lazily when someone reads, so acomputedalways sees consistent inputs and runs once per batch of writes.Then go deeper
- Dependencies are dynamic: on every run the producer list is rebuilt from what was actually read, so a signal read only in an inactive
ifbranch does not trigger anything. - Equality stops propagation: the version increases only if the new value is not equal (
Object.isby default). Acomputedwhose result does not change does not wake its consumers;{ equal }customises this for values recreated from the server. - Consequence: mutation (
items().push()) notifies nobody, because the reference and the version stay the same. Immutability is not style, it is how the graph detects change. - Effects are asynchronous and scheduled with change detection (view effects before their component is checked, root effects before all components). Signals are not an event mechanism and do not order side effects in time — that is RxJS territory.
const a = signal(1); const b = signal(2); const sum = computed(() => a() + b()); const product = computed(() => a() * b()); const result = computed(() => sum() + product()); // diamond a.set(5); b.set(3); // push: versions bump, nothing is recalculated yet result(); // pull: sum, product, result run once each, with consistent values // equality cutoff: a new array with the same content does not propagate const ids = signal<number[]>([], { equal: (x, y) => x.length === y.length && x.every((v, i) => v === y[i]), });They’ll likely ask next
Why is an
effect()that writes to another signal usually the wrong tool, and what do you use instead?Red flag
“A signal is just a BehaviorSubject with a nicer API” or “
set()re-renders the component synchronously” — it shows no understanding of laziness, versions and push-pull.- Dependencies are dynamic: on every run the producer list is rebuilt from what was actually read, so a signal read only in an inactive
S-02In a zoneless application with OnPush components, what actually schedules change detection, and what does Angular skip? Change detection
Say this first
Without zone.js nobody patches
setTimeoutor promises, so Angular renders only when it is told: a signal read in a template changes, a template/host listener fires, theasyncpipe emits,markForCheck(),setInput()or a navigation. An OnPush subtree is skipped unless one of those marks it, so the work is proportional to what changed, not to the size of the tree.Then go deeper
- The template is a reactive consumer: when a signal it read changes, Angular marks that view for refresh (like
markForCheck) and change detection reaches only the affected components. - OnPush compares inputs by reference (
===): mutating an object passed through an input does not update the child — the same immutability rule as for signals. - The classic bug: a plain field set in
setTimeout,subscribe()or a third-party callback (WebSocket, Maps) — “it only updates after I click somewhere”. The fix is a signal ortoSignal();markForCheck()is the escape hatch for legacy code. detectChanges()is synchronous and rarely needed (measurements, tests);markForCheck()is the recommended asynchronous variant.
// ✗ worked with zone.js, does nothing zoneless + OnPush export class Clock { now = new Date(); constructor() { setInterval(() => (this.now = new Date()), 1000); } } // ✓ the signal tells Angular what changed export class Clock { readonly now = signal(new Date()); constructor() { const id = setInterval(() => this.now.set(new Date()), 1000); inject(DestroyRef).onDestroy(() => clearInterval(id)); } }They’ll likely ask next
An OnPush child receives an object through an input and the parent mutates it in place. What happens and how do you fix it at the team level?
Red flag
Sprinkling
detectChanges()everywhere until the screen updates, or believing zoneless means “Angular checks less often, so it is buggier”.- The template is a reactive consumer: when a signal it read changes, Angular marks that view for refresh (like
S-03How would you migrate a large, zone.js-based application to zoneless without stopping feature work? Change detection
Say this first
Incrementally: first make components correct under OnPush with state in signals, while zone.js is still there as a safety net, then remove zone.js once nothing relies on it. The risk is not Angular, it is code and libraries that silently relied on zone.js to trigger renders.
Then go deeper
- Inventory first: plain fields set in async callbacks,
subscribe()into fields, libraries waiting forNgZone.onStable. Angular 21 ships an analysis tool (onpush_zoneless_migration, through the CLI MCP server,ng mcp). - Migrate leaf components first, feature by feature: remove
Eager, move state to signals /toSignal(), review and test. Each PR is small and reversible. - Until the end, keep
provideZoneChangeDetection({ eventCoalescing: true })and the zone.js polyfill; then drop both and watch for “updates only after a click” bugs. - Tests change too:
fakeAsyncneeds zone.js, so move toawait fixture.whenStable()and Vitest fake timers. - In a team: a lint/review rule that new code uses signals for template state, and a short checklist in the PR template, so the migrated area does not regress.
They’ll likely ask next
A third-party charting library updates data in its own callbacks. How do you integrate it without zone.js?
Red flag
“Remove zone.js in one big PR and fix whatever breaks in QA” — no inventory, no safety net, no plan for libraries and tests.
- Inventory first: plain fields set in async callbacks,
S-04How do you design the state layer of a large app: a service with signals, NgRx SignalStore or classic NgRx Store? Architecture
Say this first
The first question is not “which library” but where each piece of state lives and who owns it. A service with private signals,
computed()selectors and named methods covers most apps; SignalStore when you want an imposed shape, entities and reusable features across many teams; classic NgRx Store only when you truly need an action log, time-travel or event-driven flows.Then go deeper
- Keep state at the lowest level that needs it (component → feature → global) and lift it only when a new consumer appears. Page state is provided on the route or component, not in root, so it does not “stick” between visits.
- Server data is a cache, not your state: keep it in
httpResource/resourceand reload after writes. The URL is state too (filters, pagination). - Rules that scale: expose read-only signals (
asReadonly()), one source of truth with the rest derived, no stores injecting each other circularly — that usually signals a missing facade or a misplaced concept. - With SignalStore, only methods call
patchState,rxMethodhandles async flows (withcatchErrorinside the inner stream) andsignalStoreFeatureremoves copy-pasted loading/error code. The libraries can coexist with an existing NgRx Store.
They’ll likely ask next
Two features need the same cart data. Where does the store live, and how do they talk without importing each other?
Red flag
“Always NgRx for anything serious” or “one global store for the whole app” — tooling chosen before understanding ownership and lifetime of the state.
S-05How do you structure a large codebase into feature and shared code, and how do you enforce the boundaries in a monorepo? Architecture
Say this first
Organise by domain, not by file type: features depend on core and shared, shared depends on nothing domain-specific, and features never import each other directly. Rules that live only in a wiki break in the first week, so I put them in lint —
no-restricted-importsin a single app, or Nx tags with@nx/enforce-module-boundariesin a monorepo.Then go deeper
- Tags on two dimensions: scope (who talks to whom:
scope:shop,scope:shared) × type (dependency direction: feature → ui / data-access → util). Constraints from both apply with AND. - One public API per feature/library (a single barrel at the boundary, never in every folder); deep imports and static imports of lazy libraries are violations.
- Granularity is a trade-off: small libraries by domain and type give narrow
nx affectedgraphs and cache hits; one giant shared library makes every PR affect the whole repo. - Move to shared only what is generic, stable and already used in several features (“rule of three”); domain components used elsewhere stay in their owning feature and are exported through its public API.
'@nx/enforce-module-boundaries': ['error', { depConstraints: [ // scope: who may talk to whom { sourceTag: 'scope:shop', onlyDependOnLibsWithTags: ['scope:shop', 'scope:shared'] }, { sourceTag: 'scope:shared', onlyDependOnLibsWithTags: ['scope:shared'] }, // type: which way dependencies flow { sourceTag: 'type:feature', onlyDependOnLibsWithTags: ['type:feature', 'type:ui', 'type:data-access', 'type:util'] }, { sourceTag: 'type:ui', onlyDependOnLibsWithTags: ['type:ui', 'type:util'] }, { sourceTag: 'type:util', onlyDependOnLibsWithTags: ['type:util'] }, // ui never reaches data-access, not even transitively { sourceTag: 'type:ui', notDependOnLibsWithTags: ['type:data-access'] }, ], }],They’ll likely ask next
A developer “fixes” a lint error with
sourceTag: '*'. What do you do, and how do you stop it from happening again?Red flag
Folders by type (
components/,services/) and asharedjunk drawer, with boundaries “agreed verbally”.- Tags on two dimensions: scope (who talks to whom:
S-06How would you apply ports & adapters in Angular? Where does HTTP live, and when is it not worth it? Architecture
Say this first
The domain is plain TypeScript, use cases depend on ports written as abstract classes (which are also DI tokens), and HTTP lives only in adapters at the edge, which map DTOs to entities. The only place that binds a port to an adapter is the composition root —
app.config.tsor a route’s providers. For plain CRUD screens it is overkill.Then go deeper
- Abstract class over interface: an interface disappears at compile time, so you would need a separate
InjectionToken; the abstract class is both the type and the token. - Errors are part of the contract: the adapter translates timeouts/503 into a domain error, so no component ever sees
HttpErrorResponse. - The payoff is in testing: domain rules as pure functions, use cases with an in-memory adapter, and one contract test suite run against every adapter.
- Pragmatic variant: a pure domain for rules that matter, a port only where there are (or will be) several implementations, the rest in the usual data-access/ui structure — and enforce the layers with lint.
// application/task-repository.ts — the PORT (type + DI token) export abstract class TaskRepository { abstract list(): Promise<readonly Task[]>; abstract save(task: Task): Promise<void>; } // infrastructure/http-task-repository.ts — the ADAPTER (HttpClient + DTO mapping) @Injectable() export class HttpTaskRepository extends TaskRepository { /* … */ } // app.config.ts — the COMPOSITION ROOT providers: [{ provide: TaskRepository, useClass: HttpTaskRepository }]They’ll likely ask next
Why is
@Injectable({ providedIn: 'root', useClass: HttpTaskRepository })on the port a bad idea?Red flag
Components injecting
HttpClientor the concrete adapter directly, DTOs flowing into templates, or four layers for two forms “because Clean Architecture”.- Abstract class over interface: an interface disappears at compile time, so you would need a separate
S-07When would you choose micro-frontends with Native Federation, and when would you push back? Architecture
Say this first
Micro-frontends solve an organisational problem — autonomous teams that must deploy independently and today block each other at release — not a technical one. If I cannot name two teams that block each other, I recommend one app in a monorepo with lazy routes and enforced boundaries; MFE makes performance worse and only moves tangled code between repositories.
Then go deeper
- Valid reasons: many teams with different cadences, very large organisations, a strangler-fig migration from a legacy app, separate products under one experience.
- Angular must be a singleton with
strictVersion, ideally the exact same version everywhere: two runtimes mean components that cannot enter the shell’s tree and services with two instances. A remote on another major is isolated as a Web Component or iframe. - Treat each remote’s exposes as a semver public API; communicate via the URL, then typed events, then a few shared singleton services — never a shared global store (that is a distributed monolith).
- Plan the costs from day one: a timeout and fallback per remote, a manifest per environment, the remote name and version in every logged error, e2e on the production combination.
They’ll likely ask next
A remote fails to load in production. How should the shell behave, for a whole route versus a widget inside a page?
Red flag
“We need micro-frontends for scalability and performance” or setting
strictVersion: falseto make version warnings go away.S-08When is Angular Elements the right tool, and what are the pitfalls of shipping an Angular component as a custom element? Components
Say this first
When a component must live outside an Angular app: CMS widgets, client websites, a design system for other frameworks, or a remote that cannot share the Angular version.
createCustomElement()turns inputs into attributes/properties and outputs intoCustomEvents, but each bundle brings its own runtime (≈ 33 kB transferred) and there is no shared DI — inside the same Angular app it is almost always the wrong choice.Then go deeper
- Attributes are always strings: numeric/boolean inputs need
numberAttribute/booleanAttribute, and objects/arrays go only through properties. - Events do not bubble and are not composed: listen on the element itself. Use specific output names (
ratingChange, notchange). - Use
ViewEncapsulation.ShadowDomso the host page’s CSS does not leak in; theme through CSS variables and::part(). - Group a team’s widgets into one bundle (one
createApplication(), severaldefine()calls), use a tag different from the component selector, and do not reuse a destroyed element — create a new one.
They’ll likely ask next
Three teams each ship a widget built separately onto the same page. What does that cost, and what would you change?
Red flag
Using Angular Elements to reuse components between screens of the same Angular app, or for SEO-critical content that should be rendered on the server.
- Attributes are always strings: numeric/boolean inputs need
S-09How do you decide between CSR, SSR and prerender in an Angular app, and what does it cost to operate? SSR
Say this first
Per route, not per project:
app.routes.server.tssetsRenderMode.Prerender,ServerorClientfor each path. Prerender stable public pages, render on the server public pages with fresh data, and keep the area behind the login on the client. SSR improves LCP and SEO; it does not shrink the JavaScript or fix a slow app after hydration.Then go deeper
- Prerender with parameters needs
getPrerenderParams;PrerenderFallback.Server(the default) lets you prerender the top N products and render the rest on request. Every URL is a full render at build time. - Server mode means a Node process: TTFB is at least the slowest API called during rendering, CPU per request, and module-level state shared between users.
- Correct status codes matter for SEO: set 404 through
RESPONSE_INITinstead of a soft 404; caching is per route —public, s-maxageonly for identical content,private, no-storefor personal HTML. - In v22 configure
allowedHosts(orNG_ALLOWED_HOSTS): an unknown host gets 400 as SSRF protection. If nobody can operate a Node server, use Prerender withoutputMode: 'static'.
They’ll likely ask next
Your SSR pages became slow after a backend API degraded. Why, and what do you change?
Red flag
“Turn on SSR everywhere, it makes the app faster” — ignoring that TTI barely changes, the server cost, and personal pages cached publicly at the CDN.
- Prerender with parameters needs
S-10Explain incremental hydration and event replay in v22. How do you use them on a product page? SSR
Say this first
In v22 a plain
provideClientHydration()enables incremental hydration, and with it event replay.@defer (hydrate on …)blocks are rendered on the server but stay dehydrated in the browser — their JS is not even downloaded — until the trigger fires; clicks made before that are queued and replayed, and a click inside a dehydrated block hydrates it.Then go deeper
- Unlike a regular
@defer, the final content is already on screen, so you can defer above the fold without layout shift. hydrate neveris only for truly static content: it is not annotated, so nothing is replayed. Keep the@placeholder: hydrate triggers are ignored on client-side navigation.withIncrementalHydration()is deprecated andwithEventReplay()is redundant with the default; the opt-out iswithNoIncrementalHydration().- Do not wrap components of a few KB: the extra chunk and request cost more than they save. Use CSS states (
:active,:focus-visible) so users see that the click registered.
<app-product-header [product]="product()" /> <!-- hydrated at bootstrap --> @defer (hydrate on interaction) { <app-gallery [images]="product().images" /> <!-- JS only at the first click --> } @placeholder { <div class="gallery-skeleton"></div> <!-- shown on client-side navigation --> } @defer (hydrate on viewport) { <app-reviews [productId]="id()" /> <!-- below the fold --> } @placeholder { <div class="reviews-skeleton"></div> } @defer (hydrate never) { <app-footer /> } <!-- static HTML forever -->They’ll likely ask next
What does
hydrate whenrequire from the parent, and what happens with nested dehydrated blocks?Red flag
Putting
hydrate neveron something with listeners, or removing@placeholder“because it is never visible”.- Unlike a regular
S-11What code is not SSR-safe, and how do you prevent hydration mismatches in a team? SSR
Say this first
Anything touching
window,localStorage, layout or observers in constructors, field initialisers,ngOnInitoreffect()— those run on the server too. Browser-only work goes intoafterNextRender/afterRenderEffector behind a platform provider, and the rendered output must be identical on both platforms.Then go deeper
@if (isPlatformBrowser(…))in a template does not throw, but Angular throws away the server DOM for that block and rebuilds it — a layout shift. Prefer@deferorafterNextRender.- Mismatch causes: direct DOM manipulation (
innerHTML,appendChild), invalid HTML (<div>inside<p>), third-party scripts, a CDN stripping comments (NG0507), and values likeDate.now()that differ between platforms. - Zoneless servers do not see your own async (fetch, SDKs, timers): use
resource()orPendingTasks, otherwise the HTML leaves empty and the browser requests the data again. - State leaks: module-level variables and
useValueobjects are shared by all users of the Node process; use services oruseFactory. - In a team: an SSR build in CI, a review checklist, and
ngSkipHydrationallowed only as a documented, temporary patch.
They’ll likely ask next
The theme preference lives in
localStorage. How do you render the right theme on the server?Red flag
if (typeof window !== 'undefined')sprinkled through components, or treatingeffect()as a “browser-only hook”.S-12You inherit a slow Angular app. What is your performance strategy, and how do you keep it from regressing? Performance
Say this first
Measure first, then fix the biggest bottleneck: Core Web Vitals (LCP, INP, CLS) from real users and the Angular DevTools Profiler tell me whether the problem is loading, rendering or a busy main thread. Then I lock the gains in with bundle budgets in CI.
Then go deeper
- Loading: lazy routes,
@deferfor heavy content below the fold (never the LCP element),injectAsync()for heavy, rarely used services,NgOptimizedImagewithpriorityon the LCP image, targeted imports checked withsource-map-explorer. - Rendering: OnPush + signals,
trackon a stable id, derivations incomputed()instead of template methods; for tens of thousands of rows, CDK virtual scroll or pagination — OnPush reduces how often, not how much you render. - Main thread: continuous calculations over ~50 ms (parsing, big filters) go into a Web Worker; transfer
ArrayBuffers instead of copying huge data. - Regression control:
budgetsinangular.jsonthat fail the build, a performance check on key pages per release, and an owner for the numbers.
They’ll likely ask next
INP is bad but LCP is fine. Where do you look first?
Red flag
Optimising without measuring, or “add SSR and
@defereverywhere” — including on the main content, which makes LCP worse.- Loading: lazy routes,
S-13How does Angular protect against XSS, where are the side doors, and what is your second line of defence? Security
Say this first
Angular treats every bound value as unsafe: interpolation escapes,
[innerHTML]is sanitised, dangerous URL schemes becomeunsafe:. XSS in Angular apps almost always comes from bypassing that —bypassSecurityTrust*on untrusted data,nativeElement.innerHTML,eval— so the second line is a strict CSP with a nonce plus Trusted Types.Then go deeper
bypassSecurityTrust*is legitimate only for content you build and control (an embed URL from a validated id). A generic “safeHtml” pipe is a reusable XSS; search for these calls in every review.- CSP: Angular needs a nonce for its inline styles —
ngCspNonceon the root element or theCSP_NONCEtoken;security.autoCspcomputes hashes at build time but covers only scripts and does not work with SSR. - Roll out CSP with
Content-Security-Policy-Report-Onlyfirst, otherwise lazy chunks or inline styles break in production. - CSRF with session cookies:
HttpClientsends theXSRF-TOKENcookie asX-XSRF-TOKENon mutating, same-origin requests — but the server must set and verify it. Every frontend check is repeated on the server.
They’ll likely ask next
Product wants to render user-written Markdown. How do you do it safely?
Red flag
“Angular sanitises everything, so we are safe” while the codebase has a
bypassSecurityTrustHtmlpipe applied to CMS content.S-14Design an auth interceptor with refresh tokens. What happens when six requests get a 401 at the same time, and where do you store the tokens? HTTP
Say this first
Exactly one refresh in flight, shared by all waiting requests (a cached Observable with
shareReplay(1), cleared infinalize), then each original request is replayed with the new token. Access token in memory, refresh token in an HttpOnly, Secure, SameSite cookie — or a BFF, where the browser only has a session cookie.Then go deeper
- Naive “refresh on every 401” makes N refreshes; with refresh-token rotation they invalidate each other and some servers treat reuse as an attack and kill the session.
- Mark login/refresh with an
HttpContextToken(SKIP_AUTH) so the interceptor does not intercept itself, attach the token only to your API’s URLs, and if the token already changed since the request was sent, just retry. - Only 401 triggers a refresh — 403 means “not allowed”. A failed refresh means logout, clearing stores and caches, and propagating logout to other tabs (
BroadcastChannel). - At startup, a
provideAppInitializertries a refresh before the first route. With an external OIDC provider, use a certified library instead of hand-rolling PKCE and rotation.
private refreshInFlight: Observable<string> | null = null; refresh(): Observable<string> { this.refreshInFlight ??= this.http .post<{ accessToken: string }>('/auth/refresh', null, { withCredentials: true, // HttpOnly cookie context: new HttpContext().set(SKIP_AUTH, true), // not intercepted again }) .pipe( map((r) => r.accessToken), tap((t) => this.token.set(t)), // access token: memory only finalize(() => (this.refreshInFlight = null)), shareReplay(1), // every 401 waits for the same one ); return this.refreshInFlight; }They’ll likely ask next
Why is
shareReplay(1)acceptable here, when it is a known pitfall on other streams?Red flag
JWT in
localStoragewithout thinking about XSS or CSP, a separate refresh for every 401, or permission checks based on decoding the JWT in the frontend.S-15What is your error-handling strategy across an Angular application? Architecture
Say this first
Layers: predictable errors are handled locally next to the data (a 404 or 422 shown by the store/component), cross-cutting ones centrally in an interceptor (401, 5xx), rendering errors of an area with
@boundary(v22.2, developer preview), and everything else reaches a globalErrorHandlerthat logs to telemetry.Then go deeper
provideBrowserGlobalErrorListeners()routeswindow.onerrorand unhandled rejections toErrorHandler; keep the handler simple and safe — if it throws, you lose every error.@boundarycatches only rendering errors, not those from event handlers or async code, and a caught error is still reported toErrorHandler.- The interceptor rethrows after handling (
throwError(() => err)) so callers can still react; acatchError(() => of([]))without logging makes bugs invisible. - UX and operations: centralise and de-duplicate toasts (not five messages for one problem), translate technical errors into domain errors at the adapter, and attach context (route, version, remote) to every logged error.
They’ll likely ask next
A widget on the dashboard throws during rendering and blanks the whole page. How do you contain it?
Red flag
A
try/catchorcatchErrorin every component that swallows errors, and a toast for each one, with no central logging.S-17What testing strategy would you set for a team: what goes into unit, component and e2e tests, and what do you mock? Testing
Say this first
Test behaviour, not implementation, and push each check to the cheapest level that can catch it: pure functions and stores without the DOM, dumb components through their inputs and outputs, smart pages with a fake store and
HttpTestingController, and a few e2e flows (Playwright/Cypress) for what only the integration reveals. Mock at the boundaries — HTTP and ports — not your own child components.Then go deeper
- Since v21 the default runner is Vitest (
@angular/build:unit-test); Karma is deprecated, and old suites can be moved with the experimentalrefactor-jasmine-vitestschematic. - Zoneless tests:
await fixture.whenStable()instead offakeAsync, Vitest fake timers for debounce,TestBed.tick()to flush effects; inputs as signals viainputBinding. - With ports & adapters, run one contract test suite against every adapter; CDK harnesses keep component tests independent of the DOM structure.
- In CI:
nx affectedplus cache for speed, no cache for non-deterministic e2e against a real backend, and coverage as a signal, not a target.
They’ll likely ask next
A flaky e2e test fails one run in ten. How do you handle it in the team?
Red flag
“100% coverage with everything mocked” — tests that check private methods were called and break on every refactor while missing real bugs.
- Since v21 the default runner is Vitest (
S-18What i18n strategy would you choose: Angular’s build-time i18n or a runtime translation library? i18n
Say this first
Built-in
@angular/localizeby default: texts marked withi18n/$localize, extracted withng extract-i18n, and one optimised build per locale with no key lookup at runtime. A runtime library only when switching language without a reload is a product requirement, or translations come from a CMS without a deploy.Then go deeper
- Changing the language with built-in i18n means a reload, even with
loadTranslations()at runtime, because template texts are evaluated when components are created. - Plurals need ICU, not
n === 1 ? … : …: Romanian hasone,fewandother(“20 de fișiere”). Never concatenate sentences — word order differs between languages. - Custom ids (
@@cart.title) so translations survive source edits; dates, numbers and currency through pipes withLOCALE_IDandregisterLocaleData. - It fits SSR/prerender (one variant per locale, chosen by URL), and with hydration you add
withI18nSupport(). Start on day one: adding i18n later means hunting every hard-coded string.
They’ll likely ask next
How do you organise the workflow with professional translators and keep the XLIFF files in sync across releases?
Red flag
Hard-coded formats like
'dd/MM/yyyy'ortoFixed(2) + ' lei', string concatenation for sentences, and no custom message ids.- Changing the language with built-in i18n means a reload, even with
S-19Your company is building its own design system in Angular. How do you make accessibility a property of the system rather than of each screen? Accessibility
Say this first
Build the interactive patterns on Angular Aria (
@angular/aria, since v21): headless directives such as Tabs, Menu, Combobox, Listbox, Tree or Grid that give correct ARIA roles, keyboard navigation and focus management with zero styles, while the team owns the CSS. Then every product team gets accessibility by default instead of re-implementing tabs and dropdowns by hand.Then go deeper
- Choice of layer: Aria for a custom design system, CDK for low-level primitives (overlay, drag & drop, a11y utilities), Material when you want a ready-made UI fast.
- Style by the ARIA state the directives set (
[aria-selected='true']), so the visual state and the accessible state cannot drift apart. - Trade-offs elsewhere: virtual scroll removes off-screen rows from the DOM (Ctrl+F and screen readers do not see them); prefer pagination where full accessibility matters.
- Process: keyboard and screen-reader checks in the definition of done, automated checks in CI, and a11y review as part of the design system’s contract.
They’ll likely ask next
A team already uses Angular Material. Would you migrate them to Aria?
Red flag
“We add
aria-*attributes at the end, before release” — accessibility treated as a final checklist instead of a property of the components.S-20You lead the upgrade of a large app from Angular 21 to 22. Which breaking changes worry you, and how do you roll it out? Architecture
Say this first
One major at a time with
ng update @angular/core@22 @angular/cli@22, letting the migrations run — e.g. it addsChangeDetectionStrategy.Eagerwhere components had no strategy, so behaviour stays identical despite OnPush becoming the default. The real risk is in the runtime changes migrations cannot fully see: the fetch backend, router param inheritance and template semantics.Then go deeper
- HTTP: fetch is the default backend and
withFetch()is deprecated; upload progress needswithXhr(), andreportProgressbecomesreportUploadProgress/reportDownloadProgress. - Router and templates:
paramsInheritanceStrategy: 'always'(child routes inherit params and data from all parents),?.returnsundefinedlike in TypeScript, a stricter compiler (inputs bound twice,data-*no longer binds inputs),CanMatchFnneedscurrentSnapshot. - Platform and removals: TypeScript 6 and no Node 20;
ComponentFactoryResolver,ComponentFactoryandprovideRoutesare gone; for SSR, configureallowedHostsand dropwithIncrementalHydration(). - Rollout: an upgrade branch kept green in CI, libraries and Node/TS bumped first, e2e on critical flows, then a follow-up plan to remove
Eagerfeature by feature instead of treating it as permanent.
They’ll likely ask next
After the upgrade, a nested route suddenly receives a param it did not get before. What changed, and how do you check whether it matters?
Red flag
Editing
package.jsonversions by hand and skippingng update, or jumping several majors in one step without reading the changelog.- HTTP: fetch is the default backend and
How to answer well
- One clear sentence first, then details only if asked.
- A concrete example from a project: “we had X, we chose Y because Z”.
- Naming the trade-off and the case where you would choose differently.
- “I don’t know, but this is how I would find out” when you really don’t know.
- Reciting definitions without saying when you would use them.
- Old answers presented as current (NgModules everywhere, Zone.js as a given).
- “It depends” without saying on what.
- Inventing an API to avoid silence.