This page is for anyone facing an Angular round, from a first frontend job to a senior lead. Most rounds open with components, bindings and directives, then test dependency injection, lifecycle hooks and change detection, including OnPush and signals. Expect RxJS next: Observable versus Promise, the flattening operators, subjects and cleaning up subscriptions. Routing, guards, lazy loading, forms and interceptors fill the rest, and senior rounds add a performance problem, an upgrade story and a judgement call. Each question shows what the interviewer is checking, the shape of a strong answer and a short answer you can say out loud.
Search all questions by round, difficulty and level, or save the ones you want to practise.
Class: a TypeScript class that holds the state and the logic.
Metadata: the @Component decorator gives a selector, a template and styles.
Placement: Angular renders it wherever the selector appears in a parent template, or at bootstrap for the root.
Styles: component styles are scoped to that component by default.
"A component is a TypeScript class with an @Component decorator on top. The class holds the data and the methods, and the decorator tells Angular three main things: the selector, the template, and the styles. The selector is like a custom HTML tag, say app-user-card. When Angular compiles a parent's template and finds that tag, it creates an instance of the component there and renders its template inside it. The root component is the one exception, because it's started directly at bootstrap. The template binds to the class's fields and methods, so when the data changes the view updates. And by default the styles I write in a component only apply to that component, because Angular scopes them by adding generated attributes to its elements, so a rule in one card doesn't leak into the rest of the app."
Describing a component only as 'the files the CLI makes' without explaining the decorator, the selector or how the template links to the class.
NgModule: components are declared in a module, which imports and exports what its templates can use.
Standalone: each component lists its own dependencies in its imports array; no module needed.
Bootstrap: bootstrapApplication with provider functions such as provideRouter replaces the root module.
Why: less boilerplate, clearer dependencies, easier lazy loading; both styles can be mixed.
"In the older style, every component had to be declared in exactly one NgModule, and that module decided what its templates could use through its imports and exports. It worked, but you had to look in two places to understand one component, and shared modules tended to grow into big bags of everything. A standalone component lists what it needs directly in its own imports array: other components, directives, pipes. The app starts with bootstrapApplication and a list of providers like provideRouter and provideHttpClient instead of a root module. In recent versions standalone is the default. The wins are less boilerplate, dependencies you can see in one file, and simpler lazy loading, since a route can load a single component. Modules aren't gone, and the two mix fine, so in a migration I can convert one feature at a time."
Saying NgModules are removed and old apps must be rewritten, or not knowing where a standalone component gets its dependencies from.
Down: the parent binds data to the child's inputs.
Up: the child emits an event through an output; the parent listens with event binding.
Direct access: a view query can reach a child instance, used sparingly.
Unrelated: a shared service holding a signal or a subject.
"Data goes down through inputs and events come up through outputs. The parent writes something like [user]="selectedUser" on the child tag, and the child declares user as an input, either with the @Input decorator or the newer input() function. When the child needs to tell the parent something, say the user clicked save, it declares an output, emits a value, and the parent listens with a (saved) event binding on the child tag and receives the emitted value. The child never reaches up into the parent, which keeps it reusable. If the parent needs to call a method on the child, it can grab it with a view query, but I use that sparingly. For components that aren't parent and child, like a header and a sidebar that both show the cart count, I put the state in a shared service, as a signal or a BehaviorSubject, and both components read from it."
Passing data between siblings by digging through the parent's instance, or using a global variable on window.
Constructor: plain class creation; dependencies are injected, inputs are not set yet.
Init: ngOnChanges when bound inputs change, then ngOnInit once.
Content and view: ngAfterContentInit, then ngAfterViewInit when child views exist.
Teardown: ngOnDestroy to clean up subscriptions, timers and listeners.
"The constructor isn't really a hook, it's just the class being created, and Angular uses it to inject dependencies. At that point the inputs haven't been set yet. Then ngOnChanges runs if the component has bound inputs, before ngOnInit and again every time one of those inputs changes, with the old and new values. ngOnInit runs once, after the first round of inputs, so that's where I start work that depends on inputs, like loading data for a given id. ngDoCheck runs on every check if I need custom change tracking. After that come ngAfterContentInit and ngAfterContentChecked for projected content, then ngAfterViewInit and ngAfterViewChecked once the component's own child views exist, which is when a normal view query result is ready to use. Finally ngOnDestroy, where I clean up subscriptions, timers and event listeners."
Reading input values in the constructor, or saying ngOnInit runs every time an input changes.
Interpolation: double curly braces put a value into text.
Property binding: square brackets set a DOM or input property from the class.
Event binding: round brackets call a class method when an event fires.
Two-way: banana-in-a-box combines both, a property plus a matching Change event.
"There are four I use every day. Interpolation, with double curly braces, drops a value into text, like a user's name in a heading. Property binding uses square brackets, so [disabled]="isSaving" sets the button's disabled property from my class. Data flows from the class to the view in both of those. Event binding goes the other way: (click)="save()" calls a method when the user clicks, and I can pass the event object in if I need it. Two-way binding, the banana in a box, [(ngModel)]="name", is really property binding plus event binding together. It works for any component with an input, say size, and a matching output called sizeChange, not just ngModel. There are also special forms: [class.active] and [style.width.px] toggle one class or one style, and [attr.colspan] sets an HTML attribute that has no matching DOM property."
<h2>Hello, {{ user.name }}</h2>
<button [disabled]="isSaving" (click)="save()">Save</button>
<input [(ngModel)]="user.name" />
<div [class.active]="isActive" [attr.aria-label]="label"></div>
Mixing up the brackets, or thinking two-way binding is special magic that only ngModel can do.
Structural: adds or removes elements; the asterisk wraps the element in an ng-template.
Attribute: changes the look or behaviour of an existing element, like ngClass.
Control flow: @if, @for and @switch are built into the template syntax and replace ngIf, ngFor and ngSwitch.
track: @for requires a track expression so rows are reused instead of rebuilt.
"A structural directive changes the shape of the DOM: it adds, removes or repeats elements. ngIf and ngFor are the classic ones. The asterisk is shorthand: Angular wraps the element in an ng-template, and the directive decides whether and how many times to stamp that template out. That's also why you can only put one asterisk directive on an element. An attribute directive leaves the element in place and changes how it looks or behaves, like ngClass, ngStyle or a custom highlight directive. Newer Angular has built-in control flow: @if, @for and @switch blocks written straight in the template. They don't need imports, they read more like normal code, @if has a proper else branch, and @for makes you give a track expression, so Angular can match items by identity and reuse DOM rows instead of rebuilding the list."
Saying the asterisk is just a naming style, or thinking ngIf hides the element with CSS rather than removing it.
Selector: an attribute selector in square brackets, so it sits on any element.
Input: alias the input to the selector name so the colour goes in one attribute.
Host events: listen to mouseenter and mouseleave on the host element.
Style: set the style on the host; a host binding or Renderer2 keeps it clean.
"I'd give the directive an attribute selector, appHighlight, so I can drop it on any element. To pass the colour in the same attribute, I alias an input to the selector name, so the template reads appHighlight="lightblue", and I fall back to yellow if it's empty. Then I listen to mouseenter and mouseleave on the host element with HostListener, and set or clear the background colour. I get the element through ElementRef. Writing to nativeElement directly is fine in the browser, but if the app also renders on the server I'd rather use a host binding on style.backgroundColor or Renderer2, so Angular manages the DOM for me. Usage is just a paragraph with appHighlight on it, and I'd import the directive where it's used."
import { Directive, ElementRef, HostListener, Input } from '@angular/core';
@Directive({ selector: '[appHighlight]', standalone: true })
export class HighlightDirective {
@Input('appHighlight') color = '';
constructor(private el: ElementRef<HTMLElement>) {}
@HostListener('mouseenter') onEnter() {
this.el.nativeElement.style.backgroundColor = this.color || 'yellow';
}
@HostListener('mouseleave') onLeave() {
this.el.nativeElement.style.backgroundColor = '';
}
}
// <p appHighlight="lightblue">Hover me</p>
Using document.querySelector inside the directive to find the element instead of working with the host element Angular hands you.
Pure: the default; transform re-runs only when the input value or reference, or an argument, changes.
Impure: pure: false; transform runs on every change detection pass.
The push bug: mutating an array keeps the same reference, so a pure pipe skips it.
Fix: create a new array, or move the filtering into the component or a computed signal.
"Pipes are pure by default. A pure pipe only runs its transform again when its input changes: a new primitive value or a new object reference, or when one of its arguments changes. That makes it cheap, and it's why a pure pipe is a better choice than calling a method in the template. So if I push an item into an array, the array is still the same reference, and the pure filter pipe doesn't run again. The fix is to update immutably, like items = [...items, newItem], so the pipe sees a new reference. An impure pipe, marked pure: false, runs on every change detection cycle, which catches mutations but can be expensive in a big list. The async pipe is impure on purpose because it has to watch a subscription. I only write an impure pipe when I really need one, and I keep it very cheap."
Fixing the stale list by making the pipe impure without mentioning that it will now run on every change detection pass.
Idea: a class asks for what it needs; the injector creates and hands it over.
Asking: constructor parameters or the inject() function.
providedIn root: one shared instance for the whole app, created on first use.
Tree-shaking: if nothing injects it, the build can drop it.
"Dependency injection means a class doesn't build its own dependencies, it just asks for them, and Angular's injector creates them and hands them over. A component that needs the order service lists it in its constructor or calls inject(OrderService) in a field. The big win is that the component doesn't care how the service is made, so in a test I can swap in a fake with one provider line. providedIn: 'root' on the @Injectable decorator registers the service with the root injector. That gives me a single shared instance for the whole app, created the first time something asks for it. It's also tree-shakable: if nothing in the app injects it, the build can leave it out. That's why it's the default for most services, like an API client or an auth service that should hold one set of state."
Saying every component gets its own new service instance by default, or creating services with new inside components.
Two trees: element injectors from component providers; environment injectors for root and lazy routes.
Lookup: start at the requesting component, walk up the element tree, then the environment injectors.
Nearest wins: the first injector that has a provider supplies the instance.
Modifiers: self, skipSelf, optional and host change where the search starts or stops.
"There are two trees of injectors. Components and directives can have their own providers, which make element injectors. Then there are environment injectors: the root one, plus a child one for each lazy-loaded route that has its own providers. When a component asks for a service, Angular starts at that component's element injector and walks up through its parents. If none of them has a provider, it moves to the environment injectors, starting from the nearest one, up to root. The first match wins. So if the component itself lists the service in providers, it gets a brand new instance that lives and dies with that component. If not, but the lazy route provides it, everything in that route shares the route's copy, separate from the root one. That's a classic bug: someone adds a provider to a lazy route and suddenly the app has two copies of a service that should be a singleton."
Saying services are always singletons, or not realising that listing a service in a component's providers creates a new instance per component.
useClass: provide a different class for a token, like a mock or a new implementation.
useValue: hand over a ready-made value, like a config object.
useFactory: build the value with code, and inject what the factory needs.
useExisting: an alias, so two tokens give the same instance; InjectionToken for non-class values.
"A provider tells the injector what to hand out when someone asks for a token. useClass says use this class instead, so I can map a Logger token to a ConsoleLogger, or to a fake in tests. useValue hands out a value I already have, which is great for configuration. useFactory runs a function to build the value, and inside the factory I can call inject() to get other dependencies, for example building an API client from the config's base URL. useExisting creates an alias, so asking for the old token gives you the same instance as the new one, handy during a rename. An InjectionToken is what I use when the thing I'm providing isn't a class, like a config interface or a string. Interfaces don't exist at runtime, so they can't be a token, and the InjectionToken gives me a real, typed key."
export interface AppConfig { apiUrl: string; }
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');
providers: [
{ provide: APP_CONFIG, useValue: { apiUrl: '/api' } },
{ provide: Logger, useClass: ConsoleLogger },
{ provide: ApiClient, useFactory: () => new ApiClient(inject(APP_CONFIG).apiUrl) },
{ provide: OldLogger, useExisting: Logger },
]
// in a service
private config = inject(APP_CONFIG);
Trying to inject a TypeScript interface directly, or using useClass where useExisting was needed and ending up with two instances.
Trigger: zone.js patches async APIs and tells Angular when a task finishes.
Check: Angular walks the component tree from the root and compares each binding with its last value.
Update: only the DOM for bindings that changed is touched.
Dev check: a second pass in development catches values that change during a check.
"In the default setup, zone.js patches the browser's async APIs: DOM events, timers, promises, HTTP requests. When one of those finishes, zone.js tells Angular something might have changed, and Angular runs change detection. It walks the component tree from the root down and, for every binding in every template, compares the current value with the one it saw last time. Where they differ, it updates that bit of the DOM. Data flows one way, parent to child, in a single pass. In development mode it runs a second check straight after, and if any value changed between the two passes you get the ExpressionChangedAfterItHasBeenChecked error, which means something is updating state during rendering. The cost is that every event checks everything, so heavy getters or method calls in templates add up. OnPush and signals narrow it down, and newer apps can drop zone.js entirely and go zoneless, where signals, events and markForCheck schedule the checks."
Saying Angular rerenders the whole page on every change, or that zone.js watches variables for changes.
Triggers: a new input reference, an event in its own template or its children, the async pipe, a signal read in the template, or markForCheck.
Skipped: otherwise Angular skips it and its whole subtree.
Classic bug: mutating an input object, or setting a field in a subscribe or setTimeout.
Fix: immutable updates, async pipe, signals, or markForCheck as a last resort.
"With OnPush, Angular skips the component and everything under it unless the component has been marked dirty. It gets marked when one of its inputs gets a new reference, when an event is handled in its own template or a child's, when an async pipe in its template receives a value, when a signal it reads in its template changes, or when I call markForCheck myself. The classic surprise is mutation. The parent does user.name = 'Sam' and the OnPush child still shows the old name, because the input is the same object reference. The other one is setting a plain field inside a subscribe callback or a setTimeout: the data changes but nothing marks the view. The fix I prefer is immutable updates and letting the template read from the async pipe or a signal. markForCheck works, but if I'm sprinkling it everywhere, the data flow is wrong."
Saying OnPush components only update when inputs change, forgetting events, the async pipe and signals.
signal: a value you can read, set and update; readers are tracked.
computed: a read-only value derived from other signals, cached until they change.
effect: runs side effects, like logging or saving, when signals it reads change.
vs observables: signals always hold a current value and are synchronous; observables model events over time.
"A signal is a wrapper around a value that knows who reads it. I create one with signal(0), read it by calling it, and change it with set or update. computed builds a read-only value from other signals, like a cart total from the list of items. It's lazy and cached, and it only recalculates when one of its inputs changes. effect runs a side effect whenever the signals it reads change, like syncing to local storage or logging. I keep effects for talking to the outside world, not for copying one signal into another, because that's what computed is for. Because Angular knows exactly which templates read which signals, it can update just those components. Compared with observables, a signal always has a current value, reads are synchronous and there's nothing to unsubscribe. Observables are still better for streams of events over time, like HTTP calls or debounced search, and toSignal and toObservable connect the two."
items = signal<CartItem[]>([]);
total = computed(() => this.items().reduce((sum, i) => sum + i.price * i.qty, 0));
constructor() {
effect(() => localStorage.setItem('cart', JSON.stringify(this.items())));
}
addItem(item: CartItem) {
this.items.update(list => [...list, item]);
}
Using effect to keep a derived value in sync instead of computed, or mutating an array inside a signal and expecting readers to update.
Agree on why: simpler state and finer updates are real wins; a full rewrite is not the goal.
Right tool: signals for state the template reads; RxJS for async event streams, cancellation and timing.
Gradual: new code signal-first; convert old code when touched, using toSignal and toObservable at the edges.
Safety: tests around each change, measured results, no big-bang sprint.
"I'd start by agreeing with the goal. Signals do make component state simpler and let Angular update only what changed. But rewriting every stream in one sprint is the wrong plan, for two reasons. First, not everything should become a signal. Debounced search, cancelling old HTTP calls with switchMap, retries, websockets: RxJS is still the better tool for streams of events over time. Signals are best for state the template reads. Second, a big-bang rewrite of working code is risky and delivers nothing users see. So I'd propose a rule: new components are signal-first, existing code gets converted when we're already changing it, and toSignal and toObservable bridge the two at the edges. We'd pick one busy screen as a pilot, measure it with the profiler, and write a short team guide from what we learn. That gets the benefits without betting a sprint on it."
Either agreeing to the big-bang rewrite or dismissing signals as hype, instead of placing each tool where it fits.
Eager vs lazy: a promise starts at once; an observable starts on subscribe.
Values: one value for a promise; zero, one or many over time for an observable.
Cancel: unsubscribe stops the work; a promise can't be cancelled.
Operators: retry, debounce and switchMap compose cleanly on observables.
"A promise is eager: the work starts the moment you create it, it settles once with one value or an error, and you can't cancel it. An observable is lazy: nothing happens until someone subscribes, it can emit zero, one or many values over time, and unsubscribing tears the work down. For HttpClient that laziness matters. If I build an http.get and never subscribe, no request goes out. If I subscribe twice, two requests go out, which surprises people. And cancelling is real: when a user types a new search term, switchMap unsubscribes from the old request and the browser aborts it. On top of that I get operators like retry, debounceTime and catchError, which compose much more neatly than nested promise code. For a single one-off call a promise is fine, and I can convert with firstValueFrom when an API wants one."
Saying observables are just promises with more values, or not knowing that an HTTP observable does nothing until it's subscribed.
switchMap: cancel the old inner call when a new value arrives; search and typeahead.
mergeMap: run all inner calls at once; independent work where order doesn't matter.
concatMap: queue them one after another in order; saves that must not overlap.
exhaustMap: ignore new values while one call is running; a login or submit button.
"All four take each value and map it to an inner observable, like an HTTP call. The difference is what happens when a new value arrives while an old call is still running. switchMap drops the old one and switches to the new one, which is exactly what a search box needs, because only the latest term matters and a slow old response must never overwrite a newer one. mergeMap keeps them all running in parallel, fine for independent work like uploading several files. concatMap waits and runs them in order, so I use it for saves that must hit the server in sequence. exhaustMap ignores new values until the current call finishes, which stops a double-clicked submit button sending two orders. For the search I also debounce the input, skip repeats with distinctUntilChanged, and catch errors inside the switchMap so one failed request doesn't kill the whole stream."
results$ = this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term =>
this.api.search(term).pipe(
catchError(() => of([]))
)
)
);
// template: @for (r of results$ | async; track r.id) { <li>{{ r.name }}</li> }
Using mergeMap for a search box, which lets a slow old response land after a newer one and show the wrong results.
Subject: a multicast channel with no memory; late subscribers miss earlier values.
BehaviorSubject: needs a starting value and gives the latest one to every new subscriber.
ReplaySubject: replays the last N values to new subscribers.
Exposure: keep the subject private and expose asObservable().
"A Subject is both an observable and an observer, so I can push values into it and many subscribers get them. But it has no memory: if a component subscribes after a value was sent, it never sees that value. That's fine for one-off events like 'show a toast'. A BehaviorSubject needs a starting value and always remembers the latest one, so any new subscriber gets the current value straight away. That's what I'd use for the logged-in user, starting with null, because a component that loads later still needs to know who's logged in. A ReplaySubject replays the last N values to new subscribers and doesn't need a starting value, useful when you want a short history. In the service I keep the subject private and expose it with asObservable, so only the service can push new values. In newer code a signal often does the same job as the BehaviorSubject."
Using a plain Subject for current state and then wondering why components that load later show nothing.
Leaks: long-lived streams like intervals, store or router events and form valueChanges outlive the component.
Safe-ish: HttpClient completes after one response, but the callback can still run after destroy.
Tools: async pipe, toSignal, takeUntilDestroyed, or take(1) for one value.
Symptom: handlers firing twice after navigating back, memory growing over time.
"Any subscription to a stream that lives longer than the component needs cleaning up. That's things like interval, router events, form valueChanges, or a subject in a root service. If I don't, the component stays in memory after it's destroyed and its callback keeps running, so after navigating back and forth I see handlers firing twice or three times. HttpClient calls complete after one response, so they don't leak forever, but the callback can still run after the user has left the page. The cleanest option is not to subscribe by hand at all: use the async pipe or toSignal and let Angular manage it. When I do need a manual subscribe, I add takeUntilDestroyed, which ties it to the component's lifetime, and I call it in the constructor or pass it a DestroyRef. For one value I use take(1) or first."
Saying you never need to unsubscribe because Angular does it automatically, or unsubscribing from everything by hand with a growing list of Subscription fields.
Risks: race conditions, no cancellation, leaks and stale views with OnPush.
Fix: flatten with the right mapping operator and let the template subscribe.
Tone: explain why with a concrete failure, suggest the change, offer to pair.
Scale: if it's common, fix the pattern with a lint rule or team guide.
"I wouldn't just write 'use RxJS properly'. I'd point to what can actually go wrong. Nested subscribes have no cancellation, so if the outer value changes quickly, several inner requests race and the last one to return wins, which may be the wrong data. The inner subscription is never cleaned up, so it can leak and keep running after the user leaves. And copying into fields means an OnPush component may not update. Then I'd suggest the fix: flatten the chain with switchMap, or concatMap if order matters, and expose one observable that the template reads with the async pipe, or turn it into a signal with toSignal. I'd write that snippet in the review so it's easy to accept, and offer to pair if they want. If it's urgent and working, I might approve with a follow-up ticket. If I see the pattern a lot, it goes into our team guide and lint rules."
Blocking the change with a vague comment about best practice, or approving it without mentioning the race condition and the leak.
Types: canActivate, canActivateChild, canDeactivate, canMatch; resolvers fetch data before a route.
Functional: a plain function that uses inject() and returns true, false or a UrlTree.
Redirect: return a UrlTree to the login page instead of navigating inside the guard.
Security: guards only protect the UI; the server must check every request.
"canActivate decides if a route can be entered, canActivateChild does the same for child routes, canDeactivate asks whether you can leave, which is great for unsaved form warnings, and canMatch decides whether a route even matches, so a lazy chunk isn't downloaded for users who can't use it. Resolvers aren't guards but sit alongside them to load data before the route shows. In modern Angular a guard is just a function. For the login case I inject the auth service and the router, return true if the user is logged in, and otherwise return a UrlTree for the login page, with the original URL as a query param so I can send them back. Returning a UrlTree is cleaner than calling navigate inside the guard. And I always say this: a guard hides screens, it doesn't protect data. The API still has to check the token on every request."
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
return auth.isLoggedIn()
? true
: router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } });
};
// { path: 'orders', component: OrdersPage, canActivate: [authGuard] }
Treating a route guard as the security layer, or calling router.navigate inside a guard and returning false.
Cause: in-app navigation never hits the server; a refresh asks the server for /orders/42, which has no such file.
Fix: configure the server to return index.html for unknown paths, but not for API or asset paths.
Check: the base href matches where the app is served from.
Fallback: hash-based URLs work without server changes, at the cost of uglier links.
"This is the client-side routing classic. When users click around, the Angular router changes the URL in the browser without asking the server, so everything works. But on a refresh or a pasted link, the browser asks the server for /orders/42, and there's no file there, so the server says 404. The fix is on the server or hosting side: for any path that isn't a real file, serve index.html, and let Angular's router take over and show the right page. I'd be careful that API routes and missing assets still return real 404s, otherwise a missing script comes back as HTML and the error gets confusing. I'd also check the base href, especially if the app is served from a sub-path. If we truly couldn't change the server, hash-based URLs avoid the problem, but I'd treat that as a last resort because the links are uglier."
Blaming the Angular router config or suggesting a page reload trick, instead of recognising it's the server's fallback rule.
Route level: loadComponent or loadChildren with a dynamic import; the build puts it in its own chunk.
Preloading: optionally fetch lazy chunks in the background after the first page loads.
@defer: lazy loads part of a template, with triggers such as on viewport, on idle or on interaction.
UX: placeholder and loading blocks keep the page stable while it loads.
"Lazy loading at the route level means a feature's code isn't in the main bundle. In the route I write loadComponent or loadChildren with a dynamic import, the build splits that code into its own chunk, and the browser only downloads it the first time someone visits the route. So the admin area never slows down the login page. If I want the next pages to feel instant, I can add a preloading strategy that fetches lazy chunks in the background after the first page has loaded. @defer works inside a single template. I wrap a heavy piece, like a chart or a comments widget, in a @defer block with a trigger such as on viewport or on idle, and Angular loads its components only when the trigger fires. I give it a placeholder block and a loading block so the layout doesn't jump. Together they cut the first load a lot."
Saying lazy loading makes pages load faster everywhere, without mentioning the delay on first visit or how preloading addresses it.
Measure: Angular DevTools profiler for change detection cost; the browser Performance tab for the rest.
Less checking: OnPush or signals, and move scroll or mousemove listeners outside the zone.
Less work per check: pure pipes or computed instead of template method calls; good track keys.
Less DOM: virtual scrolling or pagination, and @defer for heavy parts.
"I'd measure first. The Angular DevTools profiler shows how often change detection runs and which components take the time, and the browser's Performance tab shows whether it's scripting, layout or painting. In tables the usual culprits are clear. Every keystroke triggers a full check, and each row calls methods in its template, like formatPrice(row), which run on every pass. I'd switch those to pure pipes or computed signals, and make the table OnPush or signal-based so typing in a search box doesn't recheck every row. I'd make sure the list tracks by a stable id, so filtering reuses rows instead of rebuilding the DOM. If there are thousands of rows, I'd render only what's visible with a virtual scroll viewport, or paginate. And for scroll or mousemove listeners that don't change state, I'd run them outside Angular's zone so they don't trigger change detection at all."
Jumping straight to fixes without measuring, or not knowing that method calls in templates run on every change detection pass.
Template-driven: ngModel in the template; Angular builds the model for you.
Reactive: FormGroup and FormControl built in the class; the template just binds to them.
Strengths: reactive is explicit, typed, easy to test and handles dynamic fields with FormArray.
Choice: template-driven for small simple forms, reactive for anything big or dynamic.
"In template-driven forms the template is the source of truth. I put ngModel on inputs, add validation attributes like required, and Angular builds the form model behind the scenes. It's quick for a login box or a small settings form. In reactive forms I build the model in the class with FormGroup, FormControl or FormBuilder, and the template just binds to it with formGroup and formControlName. That makes everything explicit: I can read and set values synchronously, listen to valueChanges as an observable, add or remove fields at runtime with FormArray, and unit test the logic without rendering anything. With typed forms, the values carry real types in the class too, instead of any. For a large form, like a multi-step checkout with conditional fields, I'd pick reactive every time. Mixing both styles in one form is what I'd avoid."
Saying template-driven forms can't be validated, or picking reactive forms for no reason other than 'it's more advanced'.
Shape: a ValidatorFn takes a control and returns an errors object or null.
Cross-field: put it on the FormGroup, not one control, so it can see both values.
Show it: read the group's error in the template, usually once the confirm field is touched.
Async: server checks use an async validator that returns an observable.
"A validator is just a function. It gets a control and returns null if it's valid, or an object describing the error if it isn't. Because this check compares two fields, I attach it to the form group rather than to either control, so it can read both values. Inside, I get password and confirm from the group and return a passwordsMismatch error when they differ. Then in the template I show a message when the group has that error and the confirm field has been touched, so the user isn't shouted at before they type. For checks that need the server, like whether a username is taken, I'd write an async validator that returns an observable. Angular only runs async validators once the sync ones pass, and I'd debounce or use updateOn blur so I'm not calling the API on every keystroke."
import { AbstractControl, ValidationErrors, ValidatorFn, Validators } from '@angular/forms';
export const passwordsMatch: ValidatorFn = (group: AbstractControl): ValidationErrors | null => {
const pass = group.get('password')?.value;
const confirm = group.get('confirm')?.value;
return pass === confirm ? null : { passwordsMismatch: true };
};
// in the component
form = this.fb.group(
{
password: ['', [Validators.required, Validators.minLength(8)]],
confirm: ['', Validators.required],
},
{ validators: passwordsMatch }
);
Putting the match check on the confirm control and reading the other field through a component variable, so it goes stale when the password changes.
Role: middleware for every HttpClient call: auth headers, logging, errors, retries.
Immutable: requests can't be edited, so clone them with the change.
Register: provideHttpClient(withInterceptors([...])); they run in the order listed.
401: catch it, refresh once or log out, and never loop on the refresh call itself.
"An interceptor sits between HttpClient and the network, so every request and response passes through it. That's the right place for things every call needs: adding an auth header, logging, turning errors into user messages. In modern Angular it's a function that gets the request and a next handler. Requests are immutable, so I clone it with setHeaders to add the bearer token, then pass the clone on. I register it with provideHttpClient and withInterceptors, and they run in the order I list them. For errors I pipe catchError onto next. On a 401 I either try a token refresh once and retry, or log the user out. One detail matters: I call inject at the top of the function, not inside the catchError callback, because inject only works in the injection context. And the refresh request must skip this logic, or a failing refresh can loop forever."
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService);
const token = auth.token();
const authReq = token
? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })
: req;
return next(authReq).pipe(
catchError((err: HttpErrorResponse) => {
if (err.status === 401) auth.logout();
return throwError(() => err);
})
);
};
// app config: provideHttpClient(withInterceptors([authInterceptor]))
Trying to set a header on the original request object, or sending the auth token to every domain including third-party APIs.
Symptom: what users or monitoring saw, like duplicate calls or a tab slowing down.
Evidence: how you narrowed it down: network tab, heap snapshots, logging subscriptions.
Cause: the exact subscription and why it outlived its component.
Prevent: the fix, plus a lint rule, a pattern or a review habit so it doesn't come back.
"At my last company we had a dashboard that got slower the longer people kept it open, and support said it sometimes saved the same filter twice. In the network tab I saw the save call fire once per visit to the page, so two visits meant two calls. That told me something was still subscribed from an earlier visit. I took heap snapshots before and after navigating away, and old instances of the filter component were still in memory. The cause was a subscription to form valueChanges merged with a stream from a root service, subscribed in ngOnInit with no teardown. The service lived forever, so it kept every old component alive. I moved the logic into the template with the async pipe, and where we needed a manual subscribe I added takeUntilDestroyed. Then I searched the codebase for the same pattern, fixed several more, and we added a lint rule and a review checklist item."
A story where the fix was a page refresh or a random guess, with no evidence of how the cause was found.
Plan: one major version at a time, following the official update guide and running the automated migrations.
Blockers: check third-party libraries first; they are usually the slowest part.
Safety: tests and a smoke checklist between each step; ship behind small, reversible releases.
Result: what broke, how you fixed it and what you'd do earlier next time.
"In my last role we had an app a few major versions behind, and it was blocking security updates. First I listed every third-party library and checked which versions supported each Angular release, because that's usually the real bottleneck. Two libraries were abandoned, so we replaced them before touching Angular itself. Then we went one major version at a time with ng update, which runs the automated code migrations, and after each step we ran the unit tests, the end-to-end suite and a manual smoke test of the key flows before merging. What broke was mostly around the edges: a date picker's styles, some deprecated APIs we had to replace by hand, and a few tests that relied on old timing behaviour. We shipped each step separately so a problem could be rolled back. Next time I'd keep the app current every release, because small upgrades are far cheaper than one big jump."
Describing a big-bang jump across several versions with no testing plan, or ignoring third-party libraries until the last minute.
Need: what was being copied or done inconsistently before.
API: inputs, outputs and content projection kept small and predictable.
Forms: ControlValueAccessor if it acts like an input, so it works with reactive forms.
Adoption: docs, examples, OnPush, and feedback from the first teams that used it.
"At my last company every team had its own version of a searchable dropdown, each with different bugs and keyboard behaviour. I built one shared component. I kept the API small: an items input, a function input to get each item's label, and an output when the selection changes. Anything visual, like how an option looks, went through content projection with a template the caller passes in, so I didn't need a new input for every design request. The important decision was implementing ControlValueAccessor, so it works with formControlName like a normal input, including disabled state and touched status for validation. I made it OnPush, added proper keyboard support and ARIA roles, and wrote a short page of examples. Two teams tried it first and their feedback changed the API before we rolled it out, which saved us breaking changes later."
A component with dozens of boolean inputs for every case, or one that can't be used inside a reactive form.
ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your resume and notes are never stored on our servers. It stays out of screen share on every plan; only you can see it.