Angular interviews lean heavily on architecture, because Angular is chosen for large applications. Expect questions on dependency injection and injector hierarchies, how change detection works and what OnPush changes, the RxJS flattening operators and when each is correct, reactive forms, route guards and lazy loading, and how to avoid subscription memory leaks. Signals and standalone components increasingly appear as well. The questions below cover the framework mechanics and the performance decisions that follow from them.
Behavioural Questions
1. Tell me about an Angular application you have worked on. How was it structured and what would you change?
Note: Angular interviews lean heavily on architecture, because Angular is chosen precisely for large applications. Talk about structure, not features.
Cover these:
- Scale and shape. Roughly how many components, routes, and developers. An application with three developers and one with thirty have completely different problems.
- How it was organised. Feature modules or standalone components, a shared or core module, lazy-loaded routes, and where state lived — services with RxJS subjects, NgRx, or component state.
- The hardest problem. Good ones: initial bundle size, change detection performance on a large table, a state management approach that grew unmanageable, or migrating across major versions.
- What you would change. This is where you show judgement. Common honest answers are that NgRx was too much for the problem, or that shared modules turned into a dumping ground.
2. How do you keep a large Angular codebase maintainable as the team grows?
Answer with conventions and automation, since those are what actually scale rather than individual discipline.
- A clear module or folder boundary per feature, with a shared area that has an owner and a rule for what may go in it. Shared modules become dumping grounds without that rule.
- Lazy load every feature route. It keeps the initial bundle small and enforces the boundary — if a feature cannot be lazy loaded, it is too entangled.
- A presentational and container split. Components that only take inputs and emit outputs are trivially testable and reusable; components that fetch data are not.
- Automate the rules. ESLint with the Angular plugin, Prettier, strict TypeScript, and a bundle-size budget that fails the build. A convention that is not enforced by CI decays.
- Generate rather than copy. Angular CLI schematics keep new code consistent by default.
Note: Mention keeping up with Angular's major releases on a schedule. Falling three versions behind is how a codebase becomes unmaintainable, and ng update makes staying current routine.
3. Describe a time you had to improve the performance of an Angular application.
Structure it as measure, diagnose, fix, verify — and be specific about which kind of slowness it was, because they have different causes.
- Slow initial load usually means bundle size. The tools are
source-map-explorerorwebpack-bundle-analyzer, and the fixes are lazy-loaded routes, removing a heavy dependency, and setting a budget inangular.jsonthat fails the build when it regresses. - Slow interaction usually means change detection. Angular Devtools' profiler shows which components are re-checked and how often. The fixes are
ChangeDetectionStrategy.OnPush, thetrackByfunction onngFor, moving work out of template expressions and getters, and virtual scrolling for long lists. - Memory growth over time usually means unclosed subscriptions.
Note: The single highest-value detail is that a function or getter called in a template runs on every change detection cycle — sometimes hundreds of times a second. Finding one of those and fixing it makes an excellent, concrete story.
4. How do you approach upgrading Angular across major versions?
Show that you treat it as routine maintenance rather than a project, because that is the only approach that works long term.
- Upgrade one major version at a time, using
ng update. Angular ships schematics that rewrite most breaking changes automatically, but only for a single-version step. - Read the update guide at update.angular.io for your exact from-and-to versions before starting. It lists what the schematics will not handle.
- Get the test suite green first. Without tests, an upgrade is a leap of faith.
- Handle third-party libraries separately. These are usually the real blocker — a library that has not released a compatible version can stop the whole upgrade, so check them before you begin.
- Keep the upgrade on its own branch with no feature work mixed in, so a rollback is clean.
Note: The strongest thing to say is that you would upgrade on a cadence — every release, or every other one. Teams that fall four versions behind end up doing a rewrite instead of an upgrade, and interviewers have usually lived through that.
5. How do you decide how much state management an Angular application needs?
This is a judgement question, and the answer they are hoping for is that you start small and escalate only on evidence.
The progression:
- Component state for anything one component owns. Most state is this, and moving it further out is a real cost.
- A service with a BehaviorSubject or a signal for state shared across a feature. This handles the large majority of applications and needs no library.
- NgRx or a similar store when you genuinely have the problems it solves: many components reading and writing the same state, a need for time-travel debugging or an audit trail of actions, or complex asynchronous coordination that Effects express better than nested subscriptions.
The cost of a store is real — actions, reducers, selectors, and effects for every piece of state, plus a learning curve for every new joiner. Adopting it for a form is a net loss.
Note: Saying you have used NgRx and would not use it again for that particular project is a strong answer. It shows you evaluate tools rather than collect them.
6. Tell me about a hard-to-reproduce RxJS bug, such as a race condition, that you tracked down in an Angular app. How did you find it?
This question checks whether you really understand asynchronous streams and can debug methodically. Choose a concrete bug and walk through your reasoning.
Structure your answer:
- The symptom: for example, a customer list sometimes showed results for the previous search term, or a Save button occasionally created two orders. Say how often it happened and how it was reported.
- Reproducing it: you throttled the network in DevTools to “Slow 3G”, which made out-of-order responses visible, or added a
tapwith timestamps to log each emission. - The root cause: name it precisely. A
mergeMaplet an older, slower HTTP response arrive after a newer one; a nestedsubscribeinside another subscribe had no cancellation; or a double click fired two requests. - The fix: switching to
switchMapto cancel stale searches,exhaustMapto ignore clicks while a save is in flight, or flattening nested subscriptions into one pipeline. - Prevention: a marble test with the
TestSchedulerthat reproduces the timing, and a lint rule banning nested subscribes.
Note: Interviewers love hearing that you wrote a test that failed before the fix. It proves you understood the timing, not just that the symptom disappeared.
7. How do you review an Angular pull request from a junior developer, and what do you look for?
The interviewer wants to know that you can raise quality and grow people at the same time. Cover both what you check and how you give feedback.
What you look for, in priority order:
- Correctness and edge cases: error handling on HTTP calls, loading and empty states, and form validation.
- Subscription hygiene: manual
subscribecalls withouttakeUntilDestroyed, nested subscribes, and places where the async pipe or a signal would be simpler. - Change detection and performance: function calls in templates, missing
trackin@forloops, and components that could useOnPush. - Architecture: business logic in components instead of services, direct DOM access instead of Angular APIs, and very large components that should be split.
- Security and accessibility: use of
bypassSecurityTrustHtml, and missing labels or keyboard support. - Tests for the behaviour that changed.
How you give feedback:
- Explain the “why” and link to the style guide or docs, so the lesson transfers.
- Mark nitpicks as non-blocking, and automate them with ESLint and Prettier.
- For a big design issue, pair on a call rather than leaving twenty comments.
- Call out what was done well.
Note: A good closing example: a recurring review comment you turned into a lint rule or a shared utility, so no one needed to repeat it again.
9. How have you convinced a team to adopt a newer Angular pattern, such as signals or standalone components, and how did you roll it out?
The interviewer is looking for influence without authority and a pragmatic, low-risk rollout rather than enthusiasm for the newest feature.
Structure your answer:
- The problem, not the feature: start with the pain. For example, NgModule boilerplate slowed onboarding and made lazy loading confusing, or OnPush components had subtle bugs because state lived in mutable service fields.
- Evidence: you converted one feature as a spike and showed the result — fewer files, a smaller lazy chunk, simpler tests, or fewer change detection bugs.
- Addressing concerns: teammates worried about stability or mixing styles. You showed the feature was stable in the current version and that standalone components and NgModules can coexist.
- Rollout plan: all new code uses the new pattern; existing code is migrated when touched; the official schematic (
ng generate @angular/core:standalone) handles bulk conversion; ESLint rules keep it consistent. - Support: a short internal guide with before-and-after examples, and pairing sessions.
- Result: the percentage migrated, and a measurable benefit.
Note: Show that you would not rewrite working code just to modernise it. Migrating when a file is already being changed keeps risk and review effort low, and business work keeps moving.
10. With a tight deadline, how do you decide what to test in an Angular feature?
The interviewer wants to see that you treat testing as risk management, not an all-or-nothing activity, and that you can defend your choices.
How to structure your answer:
- Identify the riskiest behaviour: anything involving money, permissions, data loss or complex branching. For a checkout feature, that is price calculation, the payment call and error handling — not the layout of the summary card.
- Test logic where it is cheapest: pure functions and services with plain unit tests, which need no
TestBedand run in milliseconds. - One or two component tests for the main user flow, using
HttpTestingControlleror a mocked service, asserting on what the user sees. - An end-to-end test for the critical journey with Playwright or Cypress, if one does not already exist.
- What you consciously skip: snapshot tests of markup, trivial getters, and third-party library behaviour.
- Record the gap: a ticket for follow-up tests, visible to the team, not hidden debt.
Give a concrete example: “We had two days for a coupon feature. I unit-tested the discount service across twelve cases, wrote one component test for the invalid-coupon message, and added the coupon step to our existing checkout e2e test.”
Note: Mention that a bug found in production gets a regression test first, before the fix. Over time this concentrates tests exactly where the application has proved fragile.
Technical Questions
11. What is dependency injection in Angular, and how do providers and injection scopes work?
Dependency injection means a class declares what it needs in its constructor and Angular supplies it, rather than the class constructing its own dependencies. That is what makes services swappable in tests.
@Injectable({ providedIn: 'root' })
export class OrderService {
constructor(private http: HttpClient) {}
}The injector hierarchy is the part interviewers probe. Angular resolves a dependency by walking up from the component's injector to the root injector, and the first provider it finds wins.
providedIn: 'root'— one instance for the whole application, and tree-shakable, so it is dropped from the bundle if nothing injects it. This is the default you should use.- Providing in a component's
providersarray — a new instance for each instance of that component and its children. Useful when a service holds per-component state. - Providing in a lazy-loaded module creates a separate instance for that module, which surprises people who expected a singleton.
Note: Injection tokens are worth mentioning. Since interfaces do not exist at runtime, you use an InjectionToken to inject a configuration object or a non-class value.
12. How does change detection work in Angular, and what does OnPush do?
Angular patches asynchronous APIs through Zone.js — events, timers, and XHR. When any of them completes, the zone tells Angular that something may have changed, and Angular walks the component tree from the root, re-evaluating every template binding and updating the DOM where a value differs.
The default strategy checks every component on every cycle. That is fine for a small tree and expensive for a large one.
ChangeDetectionStrategy.OnPush tells Angular to skip a component unless one of these happens:
- An
@Inputreference changes — a reference, which is why mutating an array in place does not trigger it and creating a new array does. - An event fires from within the component or its template.
- An observable bound with the
asyncpipe emits. - Change detection is triggered manually with
markForCheck().
The consequence: OnPush pushes you towards immutable data, which is a good thing anyway.
Note: Signals are the direction of travel. They let Angular know exactly which templates depend on which values, enabling fine-grained updates and eventually zoneless applications — worth mentioning as awareness of where the framework is going.
13. What is RxJS, and what is the difference between switchMap, mergeMap, concatMap and exhaustMap?
RxJS models values arriving over time as Observables. Angular uses them throughout — HttpClient, router events, and form value changes all return them.
The four flattening operators all take a value, produce an inner observable, and flatten the result. They differ in what happens when a new value arrives while an inner observable is still running:
switchMap— cancel the previous inner observable and switch to the new one. Use for a type-ahead search: you only want results for the latest keystroke, and it cancels the in-flight request.mergeMap— run them all concurrently, results arriving in whatever order they finish. Use for independent parallel work where order does not matter.concatMap— queue them and run one at a time in order. Use when order matters, such as a sequence of writes that must be applied in sequence.exhaustMap— ignore new values while one is still running. Use for a submit button: it makes double-clicks harmless.
Note: Using switchMap for a save request is a classic bug — a second click cancels the first save, which may have already reached the server.
14. What is the difference between template-driven and reactive forms in Angular?
Both produce the same result; they differ in where the source of truth lives.
Template-driven forms are built in the template with ngModel. Angular creates the form model implicitly from the directives it finds. They are quick for a login box, but the model is asynchronous to access and hard to test without rendering.
Reactive forms are built in the class:
form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', Validators.minLength(8)],
});and bound with [formGroup] and formControlName.
Why reactive is the default recommendation:
- The model is explicit and synchronously available, so it is unit-testable without a DOM.
- Validation is composed in code, and custom and asynchronous validators are straightforward.
valueChangesis an observable, so debouncing, dependent fields, and conditional validation are natural.- Dynamic forms — adding and removing controls at runtime with
FormArray— are practical rather than awkward.
Note: Mention typed reactive forms, introduced in Angular 14. They removed the long-standing complaint that form values were typed as any.
15. Explain the Angular component lifecycle hooks and when you would use each.
In the order they run:
ngOnChanges— beforengOnInitand again whenever a bound input changes. It receives aSimpleChangesobject with previous and current values. Use it to react to input changes.ngOnInit— once, after the firstngOnChanges. This is where initialisation belongs, including data fetching. The constructor should only assign injected dependencies.ngDoCheck— every change detection cycle. Powerful and dangerous; only for custom change detection.ngAfterContentInit/ngAfterContentChecked— after projected content, fromng-content, has been initialised or checked.ngAfterViewInit/ngAfterViewChecked— after the component's own view and child views are ready. This is the earliest point a@ViewChildreference is available.ngOnDestroy— just before the component is removed. Unsubscribe, clear timers, and detach event listeners here.
Note: Two traps come up repeatedly. Modifying a bound value inside ngAfterViewInit throws ExpressionChangedAfterItHasBeenCheckedError in development mode. And ngOnDestroy is the single most important hook for avoiding memory leaks.
16. How do you avoid memory leaks from RxJS subscriptions in Angular?
A subscription that outlives its component keeps the component and everything it references alive. Over a long session with many navigations, that accumulates.
The options, best first:
- The
asyncpipe. It subscribes and unsubscribes for you, and works with OnPush. If you can bind the observable straight into the template, do that and the problem disappears. takeUntilDestroyed()— since Angular 16, the cleanest programmatic answer:this.service.data$ .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(d => this.data = d);- A
destroy$subject withtakeUntil(this.destroy$), completed inngOnDestroy. The older standard pattern. - A
Subscriptionobject collecting subscriptions with.add()and unsubscribed once inngOnDestroy.
What does not need unsubscribing: observables that complete on their own, such as an HttpClient call, which emits once and completes. It is still safer to apply the same pattern uniformly rather than asking developers to remember which is which.
Note: Long-lived sources are the dangerous ones — router events, a BehaviorSubject in a root service, form valueChanges, and anything from an interval.
17. How does routing work in Angular, and what are route guards and lazy loading?
The router maps URL paths to components. You declare routes as an array, and <router-outlet> marks where the matched component is rendered. Routes are matched in order, so specific paths must come before wildcards.
Lazy loading splits a feature into its own bundle, downloaded only when the route is first visited:
{ path: 'admin', loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES) }This is the most effective single change you can make to initial load time in a large application.
Guards run before navigation completes and can allow it, block it, or redirect:
CanActivate— may this user reach this route? The usual authentication and role check.CanActivateChild— the same for child routes.CanDeactivate— may the user leave? This is how you warn about unsaved changes.CanMatch— decides whether the route matches at all, which means the lazy bundle is never even downloaded if the user is not permitted. Better thanCanActivatefor role-gated features.Resolve— pre-fetch data before the component renders, avoiding a flash of an empty view.
Note: Guards are modern functional functions now rather than classes, and a guard is a convenience, never a security control — the server must enforce authorisation regardless.
18. What is the difference between an Angular component, directive, pipe and service?
Four building blocks with four distinct jobs.
- Component — a directive with a template. It owns a piece of the UI, its own styles, and the logic that drives them. Every screen is a tree of these.
- Directive — behaviour attached to an existing element, with no template of its own. Two kinds: structural directives change the DOM layout and are prefixed with an asterisk, such as
*ngIfand*ngFor; attribute directives change appearance or behaviour, such asngClass, or a custom one that adds a tooltip. - Pipe — a value transformer used in templates, such as
{{ price | currency:'INR' }}. Pure pipes are memoised and only re-run when the input reference changes, which makes them cheap; impure pipes run on every change detection cycle and should be avoided. - Service — a class with no UI at all, holding business logic, HTTP calls, or shared state, and injected wherever needed.
The rule that ties them together: components should stay thin. If a component is making HTTP calls and transforming data, that logic belongs in a service, and the formatting belongs in a pipe.
19. How do you handle HTTP requests in Angular, and what are interceptors used for?
HttpClient returns a cold observable — the request is not sent until something subscribes, and it emits once then completes. Binding it through the async pipe is usually cleaner than subscribing manually.
getOrders(): Observable<Order[]> {
return this.http.get<Order[]>('/api/orders').pipe(
retry({ count: 2, delay: 1000 }),
catchError(this.handleError)
);
}Interceptors sit in the middle of every request and response, so cross-cutting concerns live in one place instead of in every service:
- Attaching an auth token to outgoing requests.
- Refreshing an expired token on a 401 and retrying the original request.
- Centralised error handling — turning HTTP errors into a toast or a redirect.
- A loading indicator driven by a count of in-flight requests.
- Logging and correlation ids.
Note: Requests are immutable, so an interceptor must clone rather than mutate: req.clone({ setHeaders: { Authorization: token } }). Interceptors also run in the order they are registered, which matters when one adds a header another depends on.
20. What are Angular signals, and how do they differ from RxJS observables?
A signal is a wrapper around a value that knows when it is read and notifies anything depending on it when it changes. It was introduced in Angular 16 and is the framework's direction of travel for reactivity.
count = signal(0);
double = computed(() => this.count() * 2);
increment() { this.count.update(n => n + 1); }Reading a signal is a function call. computed derives a value and recalculates lazily only when a dependency actually changed, and effect runs side effects when dependencies change.
How they differ from observables:
- Signals are synchronous and always hold a current value. Observables are streams over time and may not have emitted yet.
- Signals track dependencies automatically. You never subscribe or unsubscribe, so there is no leak to manage.
- Observables are far richer for asynchronous work — cancellation, retries, debouncing, and combining streams are what RxJS is for.
The practical division: signals for component state and derived values; RxJS for events and asynchronous streams such as HTTP and user input. toSignal and toObservable bridge the two.
Note: The reason this matters is performance. Signals tell Angular exactly which templates depend on a changed value, enabling fine-grained updates instead of walking the component tree — which is what makes zoneless Angular possible.
21. How do you bootstrap an Angular application with standalone components and no NgModules?
A standalone component declares its own template dependencies in an imports array instead of belonging to an NgModule. Since Angular 19, components are standalone by default, and the CLI generates module-free applications.
Bootstrapping uses bootstrapApplication with an application config that registers providers through provide* functions:
// main.ts
bootstrapApplication(AppComponent, appConfig);
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes, withComponentInputBinding()),
provideHttpClient(withInterceptors([authInterceptor])),
provideAnimationsAsync()
]
};
// a standalone component
@Component({
selector: 'app-root',
imports: [RouterOutlet, DatePipe, HeaderComponent],
template: '...'
})
export class AppComponent {}What replaces the old NgModule jobs:
- Compilation scope: each component's
importslist. - Root providers: the
providersarray inApplicationConfig, orprovidedIn: 'root'on services. - Lazy loading:
loadComponentfor one component, orloadChildrenpointing to a routes file. - Feature-scoped providers: the
providersproperty on a route, which creates an environment injector for that route subtree.
Existing module-based libraries still work: import an NgModule into a standalone component's imports, or use importProvidersFrom() for module providers.
Note: Benefits worth mentioning: less boilerplate, clearer dependencies for each component, easier testing because TestBed can import the component directly, and better tree shaking of unused code.
22. What is zone.js, what does it do for Angular, and what does zoneless change detection change?
zone.js monkey-patches browser asynchronous APIs — setTimeout, promises, addEventListener, XHR and fetch — so that Angular knows when any asynchronous task finishes. Angular runs the app inside NgZone, and when the zone becomes stable after an event or callback, it triggers change detection from the root. That is why a plain property assignment in a setTimeout updates the screen with no extra code.
The costs:
- Change detection runs after every async event, even ones that changed nothing, such as a mousemove handler or a third-party script's timer.
- zone.js adds bundle size and makes stack traces harder to read.
- Native
async/awaitcannot be patched, so the CLI downlevels it.
Common optimisations with zones: run noisy work outside Angular with ngZone.runOutsideAngular(), and enable event coalescing.
Zoneless change detection (stable in Angular 20) removes zone.js entirely:
providers: [provideZonelessChangeDetection()]Angular then schedules change detection only when it is explicitly notified:
- A signal read in a template changes.
- A template or host event listener fires.
- The async pipe receives a value.
markForCheck()is called, or an input changes viasetInput.
The result is fewer unnecessary checks, a smaller bundle and cleaner debugging.
Note: To prepare an app for zoneless, make components OnPush-compatible: keep template state in signals or observables with the async pipe, and never rely on a mutated field inside a timer being picked up automatically.
23. What are computed() and effect() in Angular signals, and when should you avoid using effect()?
Signals have three building blocks: a writable signal(), a derived computed(), and a side-effecting effect().
computed() creates a read-only signal derived from other signals. It tracks dependencies automatically, is lazy (it recalculates only when read) and memoised (it recalculates only when a dependency changes).
items = signal<CartItem[]>([]);
total = computed(() =>
this.items().reduce((s, i) => s + i.price * i.qty, 0));
isEmpty = computed(() => this.items().length === 0);effect() runs a function whenever the signals it reads change. It is for syncing signal state to the outside world:
constructor() {
effect(() => localStorage.setItem('cart', JSON.stringify(this.items())));
}Effects run asynchronously during change detection, must be created in an injection context, and are destroyed with their component. The onCleanup callback cancels timers or subscriptions between runs.
Avoid effect() for:
- Deriving state — setting one signal from another in an effect. Use
computed(); it is synchronous, glitch-free and cannot loop. - State that depends on a source but can also be edited — use
linkedSignal(), which resets when its source changes. - Fetching data from a signal — use
resource()orhttpResource(), which handle loading, errors and cancellation.
Good uses: logging, analytics, writing to localStorage, and syncing with a non-Angular library such as a chart or map.
Note: Use untracked() inside an effect or computed when you need to read a signal without making it a dependency, for example reading the current user id only for logging.
24. How do you convert between signals and observables in Angular using toSignal and toObservable?
Signals and RxJS solve different problems — signals hold current state, observables model events over time. The @angular/core/rxjs-interop package bridges the two.
toSignal(observable) subscribes and exposes the latest value as a signal. The subscription is removed automatically when the component or service is destroyed.
private route = inject(ActivatedRoute);
userId = toSignal(
this.route.paramMap.pipe(map(p => p.get('id'))),
{ initialValue: null }
);- Without
initialValuethe type includesundefineduntil the first emission. requireSync: truesuits observables that emit synchronously, such as aBehaviorSubject.- Errors from the observable are thrown when the signal is read.
- It must be called in an injection context, such as a field initialiser or constructor.
toObservable(signal) creates an observable that emits when the signal changes, so you can use RxJS operators on it:
query = signal('');
results = toSignal(
toObservable(this.query).pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(q => this.api.search(q))
),
{ initialValue: [] }
);It uses an effect internally, so values are emitted asynchronously, and rapid synchronous changes are collapsed into the latest value.
Rule of thumb: keep time-based logic — debouncing, cancellation, combining streams, websockets — in RxJS, and convert to a signal at the edge where the template reads it.
Note: Also mention takeUntilDestroyed and outputFromObservable from the same package. Together they remove most manual subscription management in modern Angular.
25. What are signal inputs, model inputs and the output function in modern Angular, and how do they differ from the decorators?
Since Angular 17.1–17.3, component inputs and outputs can be declared with functions instead of the @Input() and @Output() decorators.
Signal inputs — input() expose an input as a read-only signal:
export class UserCard {
user = input.required<User>();
size = input<CardSize>('sm');
disabled = input(false, { transform: booleanAttribute });
initials = computed(() => this.user().name.slice(0, 2));
}- You can derive values with
computed()directly, replacing mostngOnChangeslogic and setter inputs. input.requiredis enforced by the compiler.- They work naturally with OnPush and zoneless change detection.
Outputs — output() replace @Output() x = new EventEmitter():
saved = output<User>();
save() { this.saved.emit(this.user()); }The returned OutputEmitterRef is not an observable, so there is no risk of treating it as one; outputFromObservable() converts an existing stream.
Model inputs — model() create a writable signal that supports two-way binding. Setting it inside the child automatically emits a change event to the parent:
// child
checked = model(false);
toggle() { this.checked.update(v => !v); }
// parent template
<app-switch [(checked)]='isActive' />The parent can bind a plain property or a signal.
Note: Signal inputs are read-only inside the component. If a child needs to change a value it receives, use model() for two-way binding, or linkedSignal() for a local copy that resets when the input changes.
26. What is Angular's built-in control flow syntax, and why does the @for block require a track expression?
Angular 17 introduced built-in control flow blocks in templates, replacing the *ngIf, *ngFor and ngSwitch structural directives. They are part of the template compiler, so nothing needs importing.
@if (user(); as u) {
<p>Welcome, {{ u.name }}</p>
} @else if (loading()) {
<app-spinner />
} @else {
<a routerLink='/login'>Sign in</a>
}
@for (order of orders(); track order.id; let i = $index, last = $last) {
<app-order-row [order]='order' [index]='i' />
} @empty {
<p>No orders yet.</p>
}
@switch (status()) {
@case ('paid') { <app-badge type='success' /> }
@default { <app-badge /> }
}Advantages over the old directives:
- Cleaner syntax, a real
@else if, and an@emptyblock for lists. - Better type narrowing inside
@ifand@switch. - Faster list diffing — benchmarks show large improvements for big lists — and no directive code in the bundle.
- Works well with signals and zoneless applications.
Why track is mandatory: when the array changes, Angular must decide which DOM nodes to keep, move, create or destroy. The track expression gives each item a stable identity. With track order.id, re-fetching the list keeps existing rows, their component state and focus, and only touches what changed. Forgetting identity tracking with *ngFor was a common performance bug, so the new syntax makes it compulsory.
Use a unique id when you have one. track $index is acceptable only for static lists that never reorder.
Note: The CLI can migrate automatically with ng generate @angular/core:control-flow, and the old directives are deprecated as of Angular 20.
27. What are deferrable views with the @defer block, and which triggers can load them?
Deferrable views (Angular 17+) let you lazy-load part of a template — and all the components, directives and pipes used inside it — without touching the router. The compiler moves those dependencies into a separate chunk that is fetched only when a trigger fires.
@defer (on viewport; prefetch on idle) {
<app-reviews [productId]='id()' />
} @placeholder (minimum 300ms) {
<div class='skeleton'></div>
} @loading (after 100ms; minimum 500ms) {
<app-spinner />
} @error {
<p>Could not load reviews.</p>
}Sub-blocks:
@placeholder— shown before loading starts. Its dependencies are loaded eagerly.@loading— shown while the chunk downloads;afterandminimumprevent flicker.@error— shown if loading fails.
Triggers:
on idle— the default; loads when the browser is idle.on viewport— when the placeholder scrolls into view (uses IntersectionObserver).on interaction— on click or keydown on the placeholder or a referenced element.on hover,on immediate,on timer(2s).when condition— a custom boolean expression, such aswhen showChart().
A separate prefetch trigger downloads the code early without rendering it.
Requirements: the deferred components must be standalone and must not be referenced elsewhere in the same file, or they will be bundled eagerly.
Note: With server-side rendering, the placeholder is rendered on the server. Incremental hydration in newer versions adds hydrate triggers, so deferred server-rendered content becomes interactive only when needed.
28. What is the difference between providers and viewProviders, and how do the resolution modifiers self, skipSelf, host and optional work?
Angular has two injector hierarchies: environment injectors (root, route and platform) and element injectors created for each component or directive that declares providers. Resolution starts at the requesting element and walks up the element tree, then falls back to the environment injectors.
providers vs viewProviders on a component:
providers— visible to the component, its view (template children) and any content projected into it through <ng-content>.viewProviders— visible only to the component and its own view, not to projected content. Useful for a library component that keeps an internal service private from whatever consumers project inside it.
Resolution modifiers (passed to inject() as options, or used as parameter decorators):
| Modifier | Effect |
|---|---|
self | Look only in the current element's injector |
skipSelf | Start searching in the parent injector |
host | Stop searching at the host component's boundary |
optional | Return null instead of throwing if nothing is found |
// a nested menu finds its parent menu, not itself
parent = inject(MenuComponent, { skipSelf: true, optional: true });
// a directive requires a control on the same element
control = inject(NgControl, { self: true });Typical uses: skipSelf for recursive structures such as tree nodes or nested form groups; self in custom form controls; optional for features that work with or without a configuration token.
Note: Providing a service in a component's providers gives each component instance its own copy, destroyed with it. That is a clean way to scope state to a single widget, such as one wizard or one data table.
29. What are the useClass, useValue, useFactory and useExisting provider types in Angular, and when would you use each?
A provider tells an injector how to create the value for a token. Writing just MyService in a providers array is shorthand for { provide: MyService, useClass: MyService }. The four recipes give you control over that.
useClass— create an instance of a (possibly different) class. Great for swapping implementations by environment or in tests.useValue— supply a ready-made value such as a configuration object, a constant, or a mock.useFactory— call a function to build the value, with access to other dependencies throughinject(). Use it when creation needs logic.useExisting— make one token an alias of another, so both resolve to the same instance.
export const API_URL = new InjectionToken<string>('API_URL');
providers: [
{ provide: API_URL, useValue: environment.apiUrl },
{ provide: Logger, useClass: environment.production ? RemoteLogger : ConsoleLogger },
{
provide: StorageService,
useFactory: () => isPlatformBrowser(inject(PLATFORM_ID))
? new LocalStorageService() : new MemoryStorageService()
},
{ provide: AbstractAuth, useExisting: AuthService }
]Also know:
multi: truecollects several providers for one token into an array — howHTTP_INTERCEPTORSand validators are registered.InjectionTokenis needed for non-class values such as strings, objects and interfaces, because TypeScript interfaces do not exist at runtime.useClasstwice with the same class creates two instances;useExistingavoids that.
Note: Depending on an abstract class token and providing the concrete class with useClass is Angular's version of programming to an interface. Components never know which implementation they got, which makes testing and platform differences easy.
30. What is the inject() function in Angular, and how does it compare with constructor injection?
inject() retrieves a dependency from the current injector without declaring it as a constructor parameter. It is now the style the Angular team recommends.
@Component({ /* ... */ })
export class OrdersComponent {
private api = inject(OrderApi);
private route = inject(ActivatedRoute);
private config = inject(APP_CONFIG, { optional: true });
}Where it can be called: only in an injection context — field initialisers, the constructor, provider factories, functional guards, resolvers and interceptors, or inside runInInjectionContext(). Calling it later, for example in ngOnInit or a click handler, throws NG0203.
Advantages over constructor injection:
- Functional APIs: guards, resolvers and interceptors are plain functions, and they can only use
inject(). - Easier inheritance: subclasses no longer need to repeat every parent dependency in
super(...)calls. - Reusable helpers: you can write composable functions that inject what they need.
- Better typing for
InjectionTokenvalues, and options such asoptionalinstead of parameter decorators. - Works with the standard ECMAScript decorators and class-field semantics used by modern TypeScript.
export function injectQueryParam(name: string) {
return toSignal(inject(ActivatedRoute).queryParamMap
.pipe(map(p => p.get(name))));
}
// in any component
page = injectQueryParam('page');Constructor injection still works and is fully supported. The CLI offers a migration: ng generate @angular/core:inject.
Note: In unit tests, code using inject() is created through TestBed. For plain classes outside components, TestBed.runInInjectionContext lets you test such helpers directly.
31. What are the differences between Subject, BehaviorSubject, ReplaySubject and AsyncSubject in RxJS?
A Subject is both an observable and an observer: you can push values into it with next(), and it multicasts each value to all current subscribers. The four types differ in what a late subscriber receives.
| Type | Late subscriber gets | Typical use |
|---|---|---|
Subject | Only values emitted after it subscribes | Event buses, a destroy notifier, click streams |
BehaviorSubject | The current value immediately, then updates. Requires an initial value. | State in a service: current user, selected filter |
ReplaySubject(n) | The last n values (optionally within a time window), then updates | Caching recent messages or a value with no sensible default |
AsyncSubject | Only the final value, and only when the subject completes | Rare; a one-off result, similar to a promise |
const s = new BehaviorSubject(0);
s.next(1);
s.subscribe(v => console.log(v)); // logs 1 immediately
const r = new ReplaySubject(2);
r.next('a'); r.next('b'); r.next('c');
r.subscribe(console.log); // logs b, cBest practices in Angular services:
- Keep the subject private and expose a read-only stream with
asObservable(), so only the service can change state. BehaviorSubject.getValue()gives a synchronous snapshot, but overusing it defeats reactive design.- For new code, a writable
signal()often replaces a BehaviorSubject used as a state holder, because it is synchronous and needs no subscription.
Note: A subject that has completed or errored cannot emit again. If a service's subject errors once, every future subscriber gets the error immediately, so handle errors before they reach a long-lived state subject.
32. How do combineLatest, forkJoin, withLatestFrom and zip differ when combining observables in Angular?
All four combine several streams, but they differ in when they emit and which values they pair up.
combineLatest— waits until every source has emitted at least once, then emits an array of the latest values whenever any source emits. Ideal for view models built from filters, sort order and data.forkJoin— waits for every source to complete, then emits the last value of each once. The RxJS equivalent ofPromise.all; perfect for parallel HTTP calls. If any source errors, it errors; if any completes without emitting, it completes without emitting.withLatestFrom— an operator where only the main stream triggers emissions; it attaches the latest value of the other stream. Good for “on Save click, take the current form value”.zip— pairs values by index: first with first, second with second. Emits only when every source has a new value. Rarely needed in UI code.
// parallel page load
forkJoin({ user: api.user(id), orders: api.orders(id) })
.subscribe(({ user, orders }) => { /* ... */ });
// live view model
vm$ = combineLatest([this.filter$, this.sort$, this.products$]).pipe(
map(([f, s, list]) => applyFilterAndSort(list, f, s))
);
// action takes a snapshot of state
save$.pipe(
withLatestFrom(this.form.valueChanges),
concatMap(([, value]) => api.save(value))
);Common trap: using forkJoin with a long-lived stream such as a BehaviorSubject or valueChanges — it never completes, so forkJoin never emits. Use combineLatest or add take(1).
Note: With signals, a computed() that reads several signals often replaces combineLatest for view state, while forkJoin remains the clean choice for parallel one-shot HTTP requests.
33. How would you implement a type-ahead search box in Angular with RxJS, and which operators does it need?
A good type-ahead must avoid a request per keystroke, skip duplicate queries, cancel out-of-date requests and survive errors. Each requirement maps to an operator.
@Component({
selector: 'app-search',
imports: [ReactiveFormsModule],
template: '...'
})
export class SearchComponent {
private api = inject(ProductApi);
query = new FormControl('', { nonNullable: true });
results = toSignal(
this.query.valueChanges.pipe(
map(q => q.trim()),
debounceTime(300),
distinctUntilChanged(),
filter(q => q.length !== 1), // empty clears, 2+ searches
switchMap(q => q
? this.api.search(q).pipe(catchError(() => of([])))
: of([])
)
),
{ initialValue: [] }
);
}What each operator does:
debounceTime(300)— waits until the user pauses typing for 300 ms.distinctUntilChanged()— ignores a value identical to the previous one, such as typing and deleting a character.filter— skips queries that are too short to be useful.switchMap— unsubscribes from the previous request when a new query arrives. Angular's HttpClient aborts the in-flight request, and results can never arrive out of order.catchErrorinsideswitchMap— handles a failed request by returning an empty list. Placing it on the outer stream would complete the whole search after one error.
Converting with toSignal manages the subscription automatically. Add a loading signal with tap or finalize, and a minimum query length to protect the server.
Note: Be ready to explain why mergeMap is wrong here — a slow response for “ang” could arrive after the response for “angular” and overwrite the correct results.
35. How do you handle errors in RxJS streams in Angular without killing the stream, and where should catchError and retry go?
In RxJS, an error is terminal: once a stream errors it stops, and later values are never delivered. Where you place error handling decides whether one failure breaks a whole feature.
Key operators:
catchError(err => fallback$)— replaces the failed stream with a fallback observable, such asof([])orEMPTY, or rethrows withthrowError(() => err)after logging.retry({ count: 3, delay: 1000 })— resubscribes to the source after an error. The delay can be a function for exponential backoff. Only retry idempotent requests such as GET.finalize()— runs on completion, error or unsubscribe; ideal for clearing a loading flag.
Placement is the crucial point. In a long-lived stream, put catchError inside the flattening operator so only the inner request fails:
// wrong: the first failed save completes save$ forever
save$.pipe(
concatMap(v => this.api.save(v)),
catchError(() => EMPTY)
);
// right: each request handles its own failure
save$.pipe(
concatMap(v => this.api.save(v).pipe(
retry({ count: 2, delay: 500 }),
catchError(err => {
this.toast.error('Could not save');
return EMPTY;
})
))
);Layered strategy in Angular apps:
- An HTTP interceptor for cross-cutting concerns: 401 redirects, logging, a generic error toast.
- Local
catchErrorin services or components for feature-specific fallbacks. - A custom
ErrorHandlerprovider to report uncaught errors to a monitoring tool such as Sentry.
Note: Swallowing errors silently with EMPTY everywhere hides real problems. Always log or surface something to the user, and keep the error type information for monitoring.
36. What is a route resolver in Angular, and when would you use one instead of loading data inside the component?
A resolver fetches data before the router activates a route. Navigation waits until the resolver's observable or promise completes, and the result is made available to the component. Modern Angular uses functional resolvers:
export const productResolver: ResolveFn<Product> = (route) => {
const api = inject(ProductApi);
const router = inject(Router);
return api.get(route.paramMap.get('id')!).pipe(
catchError(() => {
router.navigate(['/not-found']);
return EMPTY;
})
);
};
{ path: 'products/:id', component: ProductPage,
resolve: { product: productResolver } }The component reads it from ActivatedRoute.data, or — with withComponentInputBinding() — directly as an input named product.
Use a resolver when:
- The page is meaningless without the data, and you prefer to show nothing new until it is ready rather than a half-empty layout.
- You want to redirect before rendering, for example to a 404 page when the record does not exist.
- With server-side rendering, the data must be present in the first HTML.
Load in the component instead when:
- You want the page shell to appear instantly with a skeleton — usually the better perceived performance, because a resolver makes the old page look frozen while the request runs.
- Data loads progressively, or different sections can load independently.
If you use resolvers, show a global progress bar by listening to router events (NavigationStart and NavigationEnd) so users know something is happening.
Note: A resolver that returns an observable which never completes, such as a BehaviorSubject, blocks navigation forever. Add take(1) or first() to long-lived streams.
37. How do you stop a user from leaving a form with unsaved changes using a CanDeactivate guard in Angular?
A CanDeactivate guard runs when the user tries to navigate away from a route. Returning false cancels the navigation; returning true allows it. It can return a boolean, a UrlTree to redirect, a promise or an observable.
A reusable pattern defines an interface that any form page can implement:
export interface HasUnsavedChanges {
hasUnsavedChanges(): boolean;
}
export const unsavedChangesGuard: CanDeactivateFn<HasUnsavedChanges> =
(component) => {
if (!component.hasUnsavedChanges()) return true;
return inject(ConfirmDialog).open('Discard your changes?');
// returns Observable<boolean>
};
// routes
{ path: 'profile/edit', component: EditProfilePage,
canDeactivate: [unsavedChangesGuard] }
// component
export class EditProfilePage implements HasUnsavedChanges {
form = inject(FormBuilder).group({ name: [''] });
hasUnsavedChanges() { return this.form.dirty; }
}Details that impress interviewers:
- After a successful save, call
form.markAsPristine()so the guard does not fire when you redirect. - The router guard only covers in-app navigation. Closing the tab, refreshing or typing a new URL leaves the Angular app, so also listen to the browser's
beforeunloadevent with@HostListener('window:beforeunload', ['$event']). - Use a styled dialog that returns an observable rather than the blocking
window.confirm. - The guard receives the current and next router state, so you can skip the prompt when moving between tabs of the same form.
Note: Browsers ignore custom text in the beforeunload prompt and show their own generic message. You can only trigger the dialog, not control its wording.
38. What are preloading strategies in the Angular router, and how would you write a custom one?
Lazy loading makes the initial bundle small, but the first visit to a lazy route then waits for its chunk to download. Preloading fetches lazy chunks in the background after the app has loaded, so later navigation feels instant.
Built-in strategies:
NoPreloading— the default; chunks load only on navigation.PreloadAllModules— preloads every lazy route once the app is stable. Simple, but wasteful for large apps or users on mobile data.
provideRouter(routes, withPreloading(PreloadAllModules))A custom strategy preloads selectively. Implement PreloadingStrategy and read a flag from route data:
@Injectable({ providedIn: 'root' })
export class SelectivePreload implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>) {
const conn = (navigator as any).connection;
if (conn?.saveData) return of(null);
return route.data?.['preload'] ? load() : of(null);
}
}
// routes
{ path: 'dashboard', loadComponent: () => import('./dashboard'),
data: { preload: true } }
provideRouter(routes, withPreloading(SelectivePreload))Other options:
- Delay preloading with a
timerso it does not compete with the first page's requests. - Quicklink-style preloading (the ngx-quicklink library) loads chunks for router links visible in the viewport.
- Deferrable views with a prefetch trigger handle lazy loading inside a page rather than between routes.
Note: A preloading strategy does not run canActivate or canMatch guards, so the chunk for a protected area can be downloaded even if the user can never open it. Never treat lazy loading as a way to hide sensitive code — authorisation must be enforced on the server.
39. What are the different ways to pass data to a route in Angular, and how does component input binding simplify reading them?
Angular offers several channels, each suited to a different kind of data:
| Mechanism | Example | Use for |
|---|---|---|
| Path parameters | /orders/42 | Required identity of the resource |
| Query parameters | /orders?status=paid&page=2 | Optional filters, sorting, pagination — shareable and bookmarkable |
| Static route data | data: { title: 'Orders' } | Config: page title, breadcrumb, required role |
| Resolved data | resolve: { order: orderResolver } | Data fetched before activation |
| Navigation state | router.navigate([...], { state: { from: 'cart' } }) | Transient info not worth putting in the URL; lost on refresh |
| A shared service or store | Signal or NgRx state | Complex objects shared across pages |
Reading them the traditional way uses ActivatedRoute — paramMap, queryParamMap and data observables. Prefer the observables to snapshot, because Angular reuses the component when only the parameters change (for example, /orders/42 to /orders/43), and a snapshot read in ngOnInit would go stale.
Component input binding removes that boilerplate:
provideRouter(routes, withComponentInputBinding())
export class OrderPage {
id = input.required<string>(); // from :id
status = input<string>(); // from ?status=
order = input.required<Order>(); // from resolve
details = computed(() => this.order().items.length);
}Path params, query params, static data and resolved data are all bound to inputs with matching names, and they update when the route changes.
Note: Never put sensitive data in query parameters: they end up in browser history, server logs and analytics tools. Use them for shareable UI state only.
40. How do you write custom synchronous and asynchronous validators for Angular reactive forms?
A validator is a function that receives an AbstractControl and returns null when valid, or an error object when invalid. The error key becomes available in control.errors for your template.
Synchronous validator with a parameter:
export function forbiddenDomain(domain: string): ValidatorFn {
return (c: AbstractControl): ValidationErrors | null =>
c.value?.endsWith('@' + domain) ? { forbiddenDomain: { domain } } : null;
}Cross-field validator — attach it to the FormGroup, not a single control:
export const passwordsMatch: ValidatorFn = (g) =>
g.get('password')?.value === g.get('confirm')?.value
? null : { passwordsMismatch: true };Asynchronous validator — returns an observable or promise, and runs only after all sync validators pass:
export function uniqueUsername(api: UserApi): AsyncValidatorFn {
return (c) => timer(400).pipe(
switchMap(() => api.isTaken(c.value)),
map(taken => taken ? { usernameTaken: true } : null),
catchError(() => of(null))
);
}
form = this.fb.group({
email: ['', [Validators.required, Validators.email, forbiddenDomain('test.com')]],
username: ['', { asyncValidators: [uniqueUsername(this.api)], updateOn: 'blur' }],
password: [''], confirm: ['']
}, { validators: passwordsMatch });Tips:
- While an async validator runs, the control's status is
PENDING— disable submit or show a spinner. updateOn: 'blur'or atimerdebounce stops a server call on every keystroke.- The observable must complete;
switchMapon a new value cancels the previous check.
Note: Client-side validation is for user experience only. The server must repeat every rule, because anyone can bypass the browser and call your API directly.
41. How do you build a dynamic form with FormArray in Angular, and what do strictly typed reactive forms add?
A FormArray holds a variable-length list of controls or groups — ideal when users can add and remove rows, such as phone numbers, education entries or invoice line items.
export class InvoiceForm {
private fb = inject(NonNullableFormBuilder);
form = this.fb.group({
customer: ['', Validators.required],
lines: this.fb.array([this.newLine()])
});
get lines() { return this.form.controls.lines; }
newLine() {
return this.fb.group({
item: ['', Validators.required],
qty: [1, [Validators.min(1)]],
price: [0]
});
}
addLine() { this.lines.push(this.newLine()); }
removeLine(i: number) { this.lines.removeAt(i); }
}In the template, bind the array with formArrayName='lines', loop over lines.controls with @for (tracking by the control object), and bind each row with [formGroupName]='i'.
Strictly typed forms (Angular 14+) infer types from initial values:
form.value.linesis typed as an array of partial line objects, so typos in field names become compile errors.form.getRawValue()returns the full type, including disabled controls —valueomits disabled controls, which is why its fields are optional.- By default
reset()sets a control tonull, so its type includesnull.NonNullableFormBuilderor{ nonNullable: true }makes reset return to the initial value and removes null from the type. FormRecordhandles dynamic keys of the same type.
Note: For very large or deeply nested dynamic forms, many teams generate forms from a JSON schema with a library such as Formly. Angular's experimental signal-based forms are also worth mentioning if the interviewer asks about the future of forms.
42. What is ControlValueAccessor, and how do you create a custom form control that works with reactive forms?
ControlValueAccessor (CVA) is the interface that bridges Angular's forms API and a UI element. Native inputs already have built-in accessors; implementing CVA lets your own component — a star rating, a date range picker, a tag input — work with formControlName and ngModel like a native input, including validation and dirty or touched state.
The four methods:
writeValue(value)— the form sets a value; update the UI.registerOnChange(fn)— store a callback; call it when the user changes the value.registerOnTouched(fn)— store a callback; call it on blur.setDisabledState(isDisabled)— optional; reflect the disabled state.
@Component({
selector: 'app-rating',
template: '...stars with (click)=select(n) and (blur)=onTouched()...',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RatingComponent),
multi: true
}]
})
export class RatingComponent implements ControlValueAccessor {
value = signal(0);
disabled = signal(false);
private onChange = (v: number) => {};
onTouched = () => {};
writeValue(v: number) { this.value.set(v ?? 0); }
registerOnChange(fn: any) { this.onChange = fn; }
registerOnTouched(fn: any) { this.onTouched = fn; }
setDisabledState(d: boolean) { this.disabled.set(d); }
select(n: number) {
if (this.disabled()) return;
this.value.set(n);
this.onChange(n);
}
}Usage is then identical to a native control: <app-rating formControlName='score' />.
Tips: do not call onChange inside writeValue, or programmatic updates will mark the form dirty; make the control keyboard-accessible with proper roles; and register NG_VALIDATORS too if the control has built-in validation rules.
Note: Instead of the NG_VALUE_ACCESSOR provider, a component can inject NgControl with self: true and set ngControl.valueAccessor = this. That also gives it access to the control's errors to display them internally.
43. How do you create a custom pipe in Angular, and why do pure pipes perform better than method calls in templates?
A pipe transforms a value for display in a template. You create one by implementing PipeTransform:
@Pipe({ name: 'truncate' })
export class TruncatePipe implements PipeTransform {
transform(value: string, max = 50, suffix = '…'): string {
if (!value) return '';
return value.length > max ? value.slice(0, max).trimEnd() + suffix : value;
}
}Import it into a standalone component's imports array and apply it in the template as {{ job.description | truncate: 120 }}. Extra arguments follow the pipe name, separated by colons, and pipes can be chained.
Why pure pipes are fast: pipes are pure by default. Angular calls transform only when the input value or an argument changes by reference (primitives by value). A method call in a template, such as {{ formatPrice(item.price) }}, runs on every change detection cycle, even when nothing changed. A pure pipe is effectively memoised per binding.
Impure pipes (pure: false) run on every change detection cycle. They are needed only when the output depends on something other than the inputs — for example, mutable arrays pushed in place or internal state. The built-in async pipe is impure because it tracks a subscription. Keep impure pipes cheap.
Good pipe candidates: formatting (currency in Indian lakh notation, relative time, file sizes), masking (phone or PAN numbers), and safe lookups. Avoid pipes that call HTTP or contain business logic — that belongs in services.
Note: Because pure pipes compare by reference, mutating an array with push will not update a pure filter pipe. Create a new array instead — the same immutability rule that OnPush components rely on. For state already in signals, a computed() is often an alternative.
44. How do you write a custom attribute directive in Angular that responds to events and changes host element styles?
An attribute directive adds behaviour or appearance to an existing element without changing its structure. You apply it like an attribute, and it can read inputs, listen to host events and bind host properties.
Example: a directive that highlights on hover and blocks double submits.
@Directive({
selector: '[appHighlight]',
host: {
'(mouseenter)': 'active.set(true)',
'(mouseleave)': 'active.set(false)',
'[style.backgroundColor]': 'active() ? color() : null',
'[class.is-active]': 'active()'
}
})
export class HighlightDirective {
color = input('lightyellow', { alias: 'appHighlight' });
active = signal(false);
}
@Directive({ selector: 'button[appSingleClick]' })
export class SingleClickDirective {
private el = inject(ElementRef<HTMLButtonElement>);
@HostListener('click')
onClick() {
this.el.nativeElement.disabled = true;
setTimeout(() => (this.el.nativeElement.disabled = false), 1500);
}
}Usage: <p appHighlight='lightblue'>Hover me</p>.
Key tools:
hostmetadata — the recommended way to bind host events, properties, attributes and classes. The@HostListenerand@HostBindingdecorators still work.ElementRef— access to the native element; use sparingly.Renderer2— DOM changes that stay safe with server-side rendering.- Selector scoping such as
button[appSingleClick]restricts where the directive applies. - Host directives (
hostDirectives) compose directives into a component without consumers adding them.
Note: Prefer host bindings over direct nativeElement manipulation. They work with server-side rendering and change detection, while direct DOM writes can be overwritten by Angular or break when rendered on the server.
45. What is the difference between ViewChild and ContentChild in Angular, and when is each query available?
Both are queries that give a component a reference to an element, directive or child component. The difference is where they look.
- View queries (
viewChild,viewChildren) search the component's own template. - Content queries (
contentChild,contentChildren) search the projected content — the markup a parent places between the component's tags and that appears through <ng-content>.
// tabs.component template: <div class='bar'>...</div><ng-content />
@Component({ selector: 'app-tabs' /* ... */ })
export class TabsComponent {
bar = viewChild.required<ElementRef>('bar'); // own template
tabs = contentChildren(TabComponent); // projected tabs
count = computed(() => this.tabs().length);
}
// parent usage
<app-tabs>
<app-tab title='Profile'>...</app-tab>
<app-tab title='Settings'>...</app-tab>
</app-tabs>When results are available:
| Query | First reliable hook |
|---|---|
| Content queries | ngAfterContentInit |
| View queries | ngAfterViewInit |
Either, with static: true (decorator form) | ngOnInit, if the element is not inside a conditional or loop |
The signal-based query functions (Angular 17.2+) return signals that update automatically as content appears or disappears, so you can use them in computed() and effect() without lifecycle hooks. The decorator forms, @ViewChild and @ContentChildren with QueryList, still work.
The read option picks what to return from a matched element — ElementRef, ViewContainerRef or a directive instance.
Note: Content queries are the backbone of compound components such as tabs, accordions and data tables, where the parent component coordinates children that the consumer declares.
46. How would you write an Angular interceptor that refreshes an expired access token and retries the failed request?
The goal: when an API call fails with 401 Unauthorized because the short-lived access token expired, get a new token using the refresh token, then replay the original request — transparently to the component. The tricky part is concurrency: if five requests fail at once, you must refresh only once and make the others wait.
A functional interceptor:
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService);
if (req.url.includes('/auth/refresh')) return next(req);
return next(withToken(req, auth.token())).pipe(
catchError((err: HttpErrorResponse) => {
if (err.status !== 401) return throwError(() => err);
return auth.refreshOnce().pipe( // shared refresh
switchMap(token => next(withToken(req, token))),
catchError(e => { auth.logout(); return throwError(() => e); })
);
})
);
};
const withToken = (r: HttpRequest<unknown>, t: string) =>
r.clone({ setHeaders: { Authorization: `Bearer ${t}` } });Inside AuthService, refreshOnce() returns the same in-flight observable to every caller, using shareReplay(1) and clearing it with finalize when done. That guarantees one refresh call for any number of simultaneous 401s.
Points to mention:
- Skip the refresh endpoint itself, or a failed refresh loops forever.
- Requests are immutable, so always
clone()to add headers. - Register with
provideHttpClient(withInterceptors([authInterceptor])); order matters, as interceptors run in array order for requests. - Store the refresh token in an
HttpOnly,Secure,SameSitecookie rather than localStorage, so XSS cannot steal it. - Only attach tokens to your own API origin, never to third-party URLs.
Note: If the refresh itself fails, clear state and send the user to login with a return URL, so they come back to the same page after signing in.
47. Explain the core building blocks of NgRx Store and how data flows between actions, reducers, selectors and effects.
NgRx implements the Redux pattern for Angular: a single immutable state tree, changed only by dispatching actions to pure functions. The data flow is one-directional:
- A component dispatches an action describing what happened.
- Reducers — pure functions — compute the new state from the current state and the action.
- Selectors read slices of state; components subscribe to them or read them as signals.
- Effects listen for actions, perform side effects such as HTTP calls, and dispatch new actions with the result.
export const ProductsActions = createActionGroup({
source: 'Products Page',
events: { 'Load': emptyProps(), 'Load Success': props<{ items: Product[] }>() }
});
export const productsFeature = createFeature({
name: 'products',
reducer: createReducer(initialState,
on(ProductsActions.load, s => ({ ...s, loading: true })),
on(ProductsActions.loadSuccess, (s, { items }) => ({ ...s, items, loading: false }))
)
});
export const loadProducts = createEffect(
(actions$ = inject(Actions), api = inject(ProductApi)) =>
actions$.pipe(
ofType(ProductsActions.load),
switchMap(() => api.list().pipe(
map(items => ProductsActions.loadSuccess({ items })),
catchError(() => of(ProductsActions.loadFailure()))
))
),
{ functional: true }
);
// component
products = this.store.selectSignal(productsFeature.selectItems);Why the rules matter:
- Immutability lets selectors and OnPush detect changes by reference.
- Memoised selectors (
createSelector) recompute only when their inputs change. - Pure reducers make state changes predictable, testable and replayable in Redux DevTools.
When it is worth it: large apps with complex shared state, many teams and a need for strong conventions. For smaller features, NgRx SignalStore or a plain service with signals is lighter.
Note: Name actions after events, such as “Products Page Opened”, not commands like “setProducts”. Event-style actions keep the log readable in DevTools and let several reducers react to the same event.
48. What do the ChangeDetectorRef methods markForCheck, detectChanges and detach do, and when would you use each?
ChangeDetectorRef gives a component manual control over its change detection. It matters most with OnPush components, which are skipped unless an input reference changes, a template event fires, the async pipe emits, or a signal read in the template changes.
markForCheck()— marks this component and all its ancestors as dirty, so they are checked during the next change detection cycle. It does not run detection immediately. Use it when data changes from a source Angular cannot see, such as a manual subscription or a third-party callback, in an OnPush component.detectChanges()— runs change detection synchronously, right now, for this component and its children only. Useful after changing state insidengAfterViewInit, or for a detached component.detach()— removes the component from the change detection tree entirely; Angular will never check it again automatically.reattach()— puts it back.checkNoChanges()— development check that throws if bindings changed.
export class TickerComponent {
private cdr = inject(ChangeDetectorRef);
price = 0;
constructor() {
this.cdr.detach(); // high-frequency feed
feed$.pipe(takeUntilDestroyed()).subscribe(p => this.price = p);
interval(1000).pipe(takeUntilDestroyed())
.subscribe(() => this.cdr.detectChanges()); // repaint once a second
}
}Choosing: markForCheck is the safe default and cooperates with the normal cycle. detach with periodic detectChanges suits dashboards and live feeds that update hundreds of times a second.
Note: With signals, most manual calls disappear: a signal read in an OnPush template marks the component for check automatically when it changes, which is why signals make zoneless apps practical.
49. What is the difference between JIT and AOT compilation in Angular, and what did the Ivy engine change?
Angular templates are not JavaScript, so they must be compiled into instructions that create and update the DOM. The question is when.
| JIT (just in time) | AOT (ahead of time) |
|---|---|
| Templates compiled in the browser at runtime | Templates compiled during the build |
| Ships the Angular compiler (large) to users | No compiler in the bundle |
| Template errors found only at runtime | Template and binding errors fail the build |
| Slower startup | Faster startup, smaller bundles |
AOT has been the default for both development and production builds since Angular 9. JIT is still used by some test setups.
Other AOT benefits:
- Security: templates are compiled from source, not from strings evaluated at runtime, removing a class of template-injection attacks.
- Type checking of templates with
strictTemplates, catching wrong property names and type mismatches on inputs. - Better tree shaking, because unused code is statically visible.
Ivy, the rendering engine default since Angular 9, changed the compiled output:
- Locality: each component compiles into a static definition (
ɵcmp) containing its own template function, so a component can be compiled without knowing the whole app. This made standalone components possible. - Tree-shakable runtime: instructions are plain functions, so features you do not use are dropped.
- Faster incremental builds, readable generated code, and better debugging with
ng.getComponent()in the console.
Note: Modern builds use the esbuild-based application builder with Vite for the dev server, giving much faster builds than the old webpack pipeline. Mention it if asked how you would speed up a slow Angular build.
50. How do you unit test an Angular component that calls an API, using TestBed and HttpTestingController?
TestBed creates an Angular testing module so a component runs with real templates, change detection and dependency injection. For HTTP, Angular provides a testing backend that intercepts requests so no real network calls are made.
describe('OrdersComponent', () => {
let fixture: ComponentFixture<OrdersComponent>;
let http: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [OrdersComponent], // standalone
providers: [provideHttpClient(), provideHttpClientTesting()]
});
fixture = TestBed.createComponent(OrdersComponent);
http = TestBed.inject(HttpTestingController);
});
afterEach(() => http.verify()); // no unexpected calls
it('renders orders from the API', () => {
fixture.detectChanges(); // triggers ngOnInit
const req = http.expectOne('/api/orders');
expect(req.request.method).toBe('GET');
req.flush([{ id: 1, total: 499 }]);
fixture.detectChanges();
const rows = fixture.nativeElement.querySelectorAll('li');
expect(rows.length).toBe(1);
});
it('shows an error message on failure', () => {
fixture.detectChanges();
http.expectOne('/api/orders').flush('fail', { status: 500, statusText: 'Error' });
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('Could not load');
});
});Other techniques to mention:
- Mock a service instead of HTTP:
{ provide: OrderApi, useValue: { list: () => of([...]) } }. This keeps component tests focused on the view. - Set inputs with
fixture.componentRef.setInput('id', 5), which works for signal inputs. - Async code:
fakeAsyncwithtick(), orawait fixture.whenStable(). - Component harnesses from the Angular CDK make tests of Material components resilient to DOM changes.
- Test services and pure functions without TestBed where possible — it is faster.
Note: Karma is deprecated. New projects use Vitest (the default in recent CLI versions) or Jest, with Playwright or Cypress for end-to-end tests. Always call http.verify() to catch requests you did not expect.