Angular Developer Interview Questions — 40 with Code and Answers
These are the questions that show up in real Angular interviews at product companies, consultancies, and FAANG-adjacent shops. Not trivia — the kind of question where they hand you a laptop and say "walk me through this." Each answer includes working TypeScript code, the reasoning interviewers want to hear, and the mistakes that get candidates eliminated.
Components and Directives
1. What is the difference between a component and a directive in Angular?
Why interviewers ask it: They want to know if you understand Angular's building blocks or if you just copy-paste components without thinking.
Answer: A component is a directive with a template. Every component is a directive, but not every directive is a component. Directives are classified into three types: components (own template), structural directives (manipulate the DOM, prefixed with *), and attribute directives (change appearance or behavior of an element).
// Attribute directive — no template, just behavior
import { Directive, ElementRef, HostListener, Input } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true
})
export class HighlightDirective {
@Input() appHighlight = 'yellow';
constructor(private el: ElementRef) {}
@HostListener('mouseenter') onMouseEnter() {
this.el.nativeElement.style.backgroundColor = this.appHighlight;
}
@HostListener('mouseleave') onMouseLeave() {
this.el.nativeElement.style.backgroundColor = '';
}
}Common mistake: Candidates say "components have templates, directives don't" — technically wrong because components ARE directives. Say "components are a subtype of directive that include a template and their own view encapsulation."
2. Explain the component lifecycle hooks in order. Which ones do you actually use?
Why interviewers ask it: They're checking if you know the sequence and if you understand when to put logic in ngOnInit versus the constructor.
Answer: The lifecycle hooks in order are:
- 1
constructor— dependency injection, nothing else - 2
ngOnChanges— fires when@Input()values change (beforengOnIniton first run) - 3
ngOnInit— component initialized, inputs available - 4
ngDoCheck— every change detection cycle - 5
ngAfterContentInit— projected content () initialized - 6
ngAfterContentChecked— after each check of projected content - 7
ngAfterViewInit— component's view and child views initialized - 8
ngAfterViewChecked— after each check of the view - 9
ngOnDestroy— cleanup before component is removed
import {
Component, OnInit, OnDestroy, OnChanges,
AfterViewInit, Input, SimpleChanges, ViewChild, ElementRef
} from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'app-user-card',
template: `<div #card>{{ user?.name }}</div>`,
standalone: true
})
export class UserCardComponent implements OnInit, OnChanges, AfterViewInit, OnDestroy {
@Input() userId!: string;
@ViewChild('card') cardEl!: ElementRef;
user: any;
private destroy$ = new Subject<void>();
constructor(private userService: UserService) {
// ONLY inject services here. Never call services here.
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['userId'] && !changes['userId'].firstChange) {
this.loadUser(); // reload when input changes
}
}
ngOnInit(): void {
this.loadUser();
}
ngAfterViewInit(): void {
// Safe to access this.cardEl here
console.log(this.cardEl.nativeElement.offsetHeight);
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
private loadUser(): void {
this.userService.getUser(this.userId)
.pipe(takeUntil(this.destroy$))
.subscribe(user => this.user = user);
}
}Common mistake: Calling HTTP services in the constructor. The constructor runs before Angular sets @Input() values and before the component is fully initialized. Always use ngOnInit.
3. What is `ViewEncapsulation` and when would you disable it?
Why interviewers ask it: CSS bugs in Angular apps are often caused by misunderstanding encapsulation. This filters for engineers who've shipped real projects.
Answer: Angular has three encapsulation modes:
Emulated(default) — Angular adds unique attribute selectors to CSS rules, scoping them to the component. No shadow DOM.ShadowDom— Uses native Shadow DOM. True isolation.None— No encapsulation. Styles become global.
import { Component, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-theme-wrapper',
template: `<ng-content></ng-content>`,
styles: [`
/* These styles will leak globally when encapsulation is None */
.mat-button { border-radius: 4px; }
`],
encapsulation: ViewEncapsulation.None // needed to style Angular Material internals
})
export class ThemeWrapperComponent {}When to use None: Styling third-party library components (Angular Material, PrimeNG) that render outside your component's shadow. Also useful for global utility class libraries you want to distribute.
Common mistake: Using ::ng-deep as a reflex. It's deprecated. If you need to pierce encapsulation intentionally, use ViewEncapsulation.None on a wrapper component scoped to that section of the UI.
4. What is `ContentChild` vs `ViewChild`? Give a real use case.
Why interviewers ask it: Tests understanding of content projection patterns — a common source of bugs in reusable components.
Answer:
ViewChild— queries elements/components defined in the component's own template.ContentChild— queries elements/components projected into the component via.
// Parent component using a reusable card
@Component({
selector: 'app-parent',
template: `
<app-card>
<app-card-header>My Title</app-card-header>
</app-card>
`
})
export class ParentComponent {}
// Card component
@Component({
selector: 'app-card',
template: `
<div class="card">
<div class="card-header">
<ng-content select="app-card-header"></ng-content>
</div>
<div class="card-body" #body>
<ng-content></ng-content>
</div>
</div>
`
})
export class CardComponent implements AfterContentInit, AfterViewInit {
// ContentChild: queries what's projected FROM the parent
@ContentChild(CardHeaderComponent) header!: CardHeaderComponent;
// ViewChild: queries elements in THIS component's own template
@ViewChild('body') bodyEl!: ElementRef;
ngAfterContentInit(): void {
// Safe to use this.header here
console.log('Header text:', this.header?.text);
}
ngAfterViewInit(): void {
// Safe to use this.bodyEl here
console.log('Body height:', this.bodyEl.nativeElement.offsetHeight);
}
}Dependency Injection
5. Explain Angular's DI system. What is a provider and what are the different ways to provide a service?
Why interviewers ask it: DI is Angular's core architecture. A shallow answer here flags someone who won't be productive on a real team.
Answer: Angular's DI system is a hierarchical injector tree. When a component requests a service, Angular walks up the injector hierarchy (component → module → root) until it finds a provider.
The four ways to provide a service:
// 1. Root-level (singleton for the entire app) — most common
@Injectable({
providedIn: 'root'
})
export class AuthService {}
// 2. Feature module (scoped to lazy-loaded module — new instance per load)
@NgModule({
providers: [DataService]
})
export class FeatureModule {}
// 3. Component-level (new instance per component instance)
@Component({
selector: 'app-form',
providers: [FormStateService] // isolated state per form component
})
export class FormComponent {}
// 4. Standalone component providers array
@Component({
standalone: true,
providers: [{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }]
})
export class AppComponent {}The useClass, useValue, useFactory, useExisting providers:
// useValue — provide a static config object
{ provide: APP_CONFIG, useValue: { apiUrl: 'https://api.example.com' } }
// useFactory — dynamic instantiation with deps
{
provide: LogService,
useFactory: (env: Environment) => env.production ? new SilentLogger() : new ConsoleLogger(),
deps: [Environment]
}
// useExisting — alias one token to another (doesn't create a second instance)
{ provide: OldService, useExisting: NewService }Common mistake: Providing a service at the component level when you want a singleton, or at root level when you want isolated state per component.
6. What is the `InjectionToken` and when do you need it?
Why interviewers ask it: Distinguishes engineers who've built configurable libraries from those who've only consumed them.
Answer: InjectionToken is used when you need to inject a non-class value (primitive, interface, object) or when you want to avoid naming collisions between providers.
import { InjectionToken, inject } from '@angular/core';
export interface AppConfig {
apiUrl: string;
timeout: number;
featureFlags: Record<string, boolean>;
}
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config', {
providedIn: 'root',
factory: () => ({
apiUrl: 'https://api.example.com',
timeout: 5000,
featureFlags: {}
})
});
// Injecting in a service
@Injectable({ providedIn: 'root' })
export class ApiService {
private config = inject(APP_CONFIG);
getData() {
return fetch(`${this.config.apiUrl}/data`);
}
}
// Overriding in tests
TestBed.configureTestingModule({
providers: [
{ provide: APP_CONFIG, useValue: { apiUrl: 'http://localhost:3000', timeout: 1000, featureFlags: {} } }
]
});7. What is the difference between `inject()` and constructor injection? When would you prefer one over the other?
Why interviewers ask it: inject() is the modern Angular pattern. If a candidate only knows constructor injection, they're behind.
Answer: Both achieve the same result. inject() (introduced in Angular 14, recommended in v16+) is a function you call in the injection context (constructor, field initializer, or factory function).
// Constructor injection — classic
@Injectable({ providedIn: 'root' })
export class OldWayService {
constructor(
private http: HttpClient,
private auth: AuthService,
private router: Router
) {}
}
// inject() function — modern, preferred
@Injectable({ providedIn: 'root' })
export class ModernService {
private http = inject(HttpClient);
private auth = inject(AuthService);
private router = inject(Router);
}
// inject() enables reusable injection patterns (composable functions)
function withAuth() {
const auth = inject(AuthService);
const router = inject(Router);
return {
checkAuth: () => {
if (!auth.isLoggedIn()) router.navigate(['/login']);
}
};
}
@Component({ standalone: true, template: '' })
export class ProtectedComponent {
private auth = withAuth(); // reusable logic, no mixin overhead
}When to prefer constructor injection: In non-standalone NgModule apps where the team standard is constructor-based. In practice, inject() is cleaner, eliminates long constructor parameter lists, and enables functional composition patterns.
RxJS Observables and Operators
8. Explain the difference between `Subject`, `BehaviorSubject`, `ReplaySubject`, and `AsyncSubject`.
Why interviewers ask it: State management in Angular services often uses subjects. Getting this wrong causes UI bugs that are hard to debug.
Answer:
import { Subject, BehaviorSubject, ReplaySubject, AsyncSubject } from 'rxjs';
// Subject — no initial value, only gets values emitted AFTER subscription
const subject = new Subject<number>();
subject.subscribe(v => console.log('A:', v)); // subscribes first
subject.next(1); // A: 1
const lateSubscriber = subject.subscribe(v => console.log('B:', v)); // subscribes after
subject.next(2); // A: 2, B: 2 — late subscriber misses 1
// BehaviorSubject — requires initial value, replays last value to new subscribers
const behavior = new BehaviorSubject<number>(0);
behavior.next(1);
behavior.subscribe(v => console.log('B:', v)); // immediately gets: B: 1
behavior.next(2); // B: 2
// ReplaySubject — replays N last values to new subscribers
const replay = new ReplaySubject<number>(3); // buffer of 3
replay.next(1); replay.next(2); replay.next(3); replay.next(4);
replay.subscribe(v => console.log('R:', v)); // gets: 2, 3, 4 (last 3)
// AsyncSubject — only emits the LAST value, and only when complete()
const async$ = new AsyncSubject<number>();
async$.subscribe(v => console.log('A:', v));
async$.next(1); async$.next(2); async$.next(3);
async$.complete(); // only now emits: A: 3Real-world usage:
BehaviorSubject— auth state, user profile, theme, any "current state" serviceReplaySubject(1)— when you wantBehaviorSubjectbehavior but can't provide an initial valueSubject— event bus, component communication where you don't need historyAsyncSubject— rare; useful for caching a single async operation result
9. What is the difference between `switchMap`, `mergeMap`, `concatMap`, and `exhaustMap`?
Why interviewers ask it: This is the most commonly asked RxJS question in senior Angular interviews. Getting it wrong directly translates to race conditions in production.
Answer: All four are higher-order mapping operators (they map each value to an inner observable). They differ in how they handle concurrent inner observables.
import { fromEvent, interval } from 'rxjs';
import { switchMap, mergeMap, concatMap, exhaustMap, take } from 'rxjs/operators';
const clicks$ = fromEvent(document, 'click');
const search$ = (term: string) => this.http.get(`/api/search?q=${term}`);
// switchMap — cancels previous inner observable when new outer value arrives
// USE FOR: search autocomplete, route data loading, "latest wins"
this.searchTerm$.pipe(
debounceTime(300),
switchMap(term => search$(term)) // if user types fast, only last request matters
).subscribe();
// mergeMap — runs all inner observables concurrently
// USE FOR: parallel HTTP requests where order doesn't matter
this.fileIds$.pipe(
mergeMap(id => this.uploadService.upload(id)) // all uploads run in parallel
).subscribe();
// concatMap — queues inner observables, waits for each to complete before starting next
// USE FOR: sequential operations where order matters (e.g., save then navigate)
this.actions$.pipe(
concatMap(action => this.api.process(action)) // processes one at a time, in order
).subscribe();
// exhaustMap — ignores new outer values while inner observable is still active
// USE FOR: form submissions, login buttons — prevents double-submit
this.loginClicks$.pipe(
exhaustMap(() => this.authService.login(credentials)) // ignores extra clicks while login in progress
).subscribe();The memory trick:
switch= cancel old, use newmerge= run everything at onceconcat= queue and waitexhaust= ignore until current finishes
10. What is `shareReplay` and when should you use it?
Why interviewers ask it: Multiple subscriptions to HTTP observables is a common Angular bug. This tests awareness of that pattern.
Answer: shareReplay(1) multicasts an observable to multiple subscribers and replays the last N emissions to late subscribers. Critical for caching HTTP calls.
@Injectable({ providedIn: 'root' })
export class UserService {
// WITHOUT shareReplay — three subscribers = three HTTP calls
getUser(id: string) {
return this.http.get<User>(`/api/users/${id}`);
}
// WITH shareReplay — three subscribers = one HTTP call, result cached
getUser$(id: string) {
return this.http.get<User>(`/api/users/${id}`).pipe(
shareReplay(1)
);
}
}
// Even better — cache at service level with a Map
@Injectable({ providedIn: 'root' })
export class CachedUserService {
private cache = new Map<string, Observable<User>>();
getUser(id: string): Observable<User> {
if (!this.cache.has(id)) {
this.cache.set(id,
this.http.get<User>(`/api/users/${id}`).pipe(shareReplay(1))
);
}
return this.cache.get(id)!;
}
}shareReplay vs shareReplay({ bufferSize: 1, refCount: true }): The refCount: true option unsubscribes from the source when all subscribers unsubscribe (better for memory). Default refCount: false keeps the source alive forever — which is what you want for a cache, but can cause memory leaks if misused.
11. How do you avoid memory leaks from subscriptions?
Why interviewers ask it: Memory leaks are one of the most common Angular performance problems. This is a seniority filter.
Answer: Four patterns, from worst to best:
// Pattern 1: Manual unsubscribe (error-prone, easy to forget)
export class OldComponent implements OnDestroy {
private sub: Subscription;
ngOnInit() {
this.sub = this.data$.subscribe(d => this.data = d);
}
ngOnDestroy() {
this.sub.unsubscribe();
}
}
// Pattern 2: takeUntil + destroy subject (most widely used)
export class BetterComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
ngOnInit() {
this.data$.pipe(takeUntil(this.destroy$)).subscribe(d => this.data = d);
this.other$.pipe(takeUntil(this.destroy$)).subscribe(x => this.x = x);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
// Pattern 3: takeUntilDestroyed (Angular 16+) — cleanest
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({ standalone: true, template: '' })
export class ModernComponent {
data: any;
constructor() {
inject(DataService).data$
.pipe(takeUntilDestroyed()) // automatically ties to component lifetime
.subscribe(d => this.data = d);
}
}
// Pattern 4: async pipe (template-driven, no manual unsubscribe needed)
@Component({
template: `
<div *ngIf="data$ | async as data">{{ data.name }}</div>
`
})
export class AsyncPipeComponent {
data$ = this.dataService.data$; // async pipe manages subscription
constructor(private dataService: DataService) {}
}Best practice in 2024+: Prefer async pipe for template data + takeUntilDestroyed() for service-level subscriptions. The destroy$ subject pattern still works but is boilerplate you no longer need.
Change Detection
12. What is the difference between `Default` and `OnPush` change detection?
Why interviewers ask it: OnPush is the single most impactful performance optimization in Angular. Interviewers use this to separate seniors from juniors.
Answer: Angular's change detection checks whether the view needs to be updated.
- Default: Angular checks every component in the tree on every event (click, HTTP response, setTimeout, Promise resolution). Safe but slow for large trees.
- OnPush: Angular only checks a component when:
- 1An
@Input()reference changes (not just mutation) - 2An event originates from the component or its children
- 3An observable via
asyncpipe emits - 4You manually call
markForCheck()ordetectChanges()
import { Component, ChangeDetectionStrategy, Input, OnInit } from '@angular/core';
import { ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'app-user-list',
template: `
<ul>
<li *ngFor="let user of users">{{ user.name }}</li>
</ul>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserListComponent {
@Input() users: User[] = []; // must pass a NEW array reference, not mutate existing one
constructor(private cdr: ChangeDetectorRef) {}
}
// WRONG — OnPush won't detect this
this.users.push(newUser); // mutates array, same reference
// CORRECT — pass new reference
this.users = [...this.users, newUser];
// When you must update from outside Angular zone (e.g., WebSocket)
export class RealtimeComponent implements OnInit {
data: any;
constructor(private ws: WebSocketService, private cdr: ChangeDetectorRef) {}
ngOnInit() {
this.ws.messages$.subscribe(msg => {
this.data = msg;
this.cdr.markForCheck(); // tell Angular this component needs checking
});
}
}Interview tip: Always use OnPush by default in new components. Enable it in your schematic defaults. The performance difference in large lists is dramatic.
13. What is `Zone.js` and what does "zoneless Angular" mean?
Why interviewers ask it: Angular 17-18 introduced experimental zoneless mode. This tests if the candidate follows the framework's direction.
Answer: Zone.js patches browser APIs (setTimeout, Promise, addEventListener, XHR) to notify Angular when async operations complete, triggering change detection. It's what makes Default change detection "just work" — you don't call detectChanges() manually because Zone.js does it for you.
The problem with Zone.js: It adds ~35KB, patches every async API globally (including third-party code), and makes debugging harder (stack traces go through Zone.js wrappers).
Zoneless Angular (v18 stable, v17 experimental):
// main.ts — opt into zoneless
import { bootstrapApplication } from '@angular/platform-browser';
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
bootstrapApplication(AppComponent, {
providers: [
provideExperimentalZonelessChangeDetection()
]
});
// package.json — remove Zone.js from polyfills
// angular.json: remove "zone.js" from polyfills array
// Without Zone.js, you MUST use signals or async pipe for reactivity
@Component({
template: `<div>{{ count() }}</div>`, // signal — auto-tracks
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
count = signal(0);
increment() { this.count.update(c => c + 1); } // no manual cdr needed
}Angular Signals (v17+)
14. What are Angular Signals and how do they differ from RxJS observables?
Why interviewers ask it: Signals are Angular's biggest architecture change since v2. Any Angular candidate for a v17+ project needs to know this.
Answer: Signals are synchronous, reactive state primitives. An observable is a stream of values over time. A signal always has a current value.
import { signal, computed, effect, Signal } from '@angular/core';
// signal() — writable reactive state
const count = signal(0);
console.log(count()); // read: 0
count.set(5); // write
count.update(c => c + 1); // transform
// computed() — derived state, lazy and memoized
const doubled = computed(() => count() * 2);
console.log(doubled()); // 10
// effect() — side effects that run when signals change
effect(() => {
console.log(`Count changed to: ${count()}`);
// runs immediately and whenever count changes
});
// In a component
@Component({
standalone: true,
template: `
<p>Count: {{ count() }}</p>
<p>Doubled: {{ doubled() }}</p>
<button (click)="increment()">+</button>
`
})
export class CounterComponent {
count = signal(0);
doubled = computed(() => this.count() * 2);
increment() {
this.count.update(c => c + 1);
}
}Signals vs RxJS:
| | Signal | Observable |
|---|---|---|
| Always has value | Yes | No (optional) |
| Synchronous | Yes | Can be async |
| Template integration | Automatic (no async pipe) | Needs async pipe |
| Composition | computed() | pipe(map(), filter()...) |
| Use for | UI state, derived data | Async streams, events, HTTP |
They're not replacements — use signals for component state, observables for HTTP and event streams, and bridge them with toSignal() / toObservable().
15. What are `toSignal()` and `toObservable()` and how do you use them?
Why interviewers ask it: Real apps mix both paradigms. Knowing the bridge utilities is practical.
Answer:
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { signal, computed } from '@angular/core';
@Component({
standalone: true,
template: `
@if (user(); as user) {
<p>{{ user.name }}</p>
}
@if (error()) {
<p>Error: {{ error() }}</p>
}
`
})
export class UserComponent {
private userId = signal(1);
private http = inject(HttpClient);
// toSignal — converts observable to signal
// requires injection context (constructor or field initializer)
user = toSignal(
toObservable(this.userId).pipe(
switchMap(id => this.http.get<User>(`/api/users/${id}`))
),
{ initialValue: null }
);
// error handling
private userResult = toSignal(
this.http.get<User>('/api/user').pipe(
map(user => ({ user, error: null })),
catchError(err => of({ user: null, error: err.message }))
)
);
user2 = computed(() => this.userResult()?.user);
error = computed(() => this.userResult()?.error);
}toSignal options:
initialValue— value before the observable emitsrequireSync— throws if observable doesn't emit synchronously (useful withBehaviorSubject)injector— use outside injection context by passing an injector
16. What is `input()` signal vs `@Input()` decorator in Angular 17+?
Why interviewers ask it: Angular 17.1 introduced signal inputs. This is now the preferred pattern.
Answer:
import { Component, input, output, model } from '@angular/core';
// Old way
@Component({ selector: 'app-old' })
export class OldComponent {
@Input({ required: true }) name!: string;
@Input() age = 0;
@Output() changed = new EventEmitter<string>();
}
// New way — signal inputs (v17.1+)
@Component({
selector: 'app-new',
standalone: true,
template: `
<p>{{ name() }}</p>
<p>{{ nameUppercase() }}</p>
`
})
export class NewComponent {
name = input.required<string>(); // required signal input
age = input(0); // optional with default — type inferred as Signal<number>
title = input('', { alias: 'cardTitle' }); // aliased
// derived from input — no separate computed needed
nameUppercase = computed(() => this.name().toUpperCase());
// output() replaces @Output() + EventEmitter
changed = output<string>();
// model() — two-way binding (replaces @Input + @Output value/valueChange pattern)
value = model('');
emitChange() {
this.changed.emit(this.name());
}
}Advantages of signal inputs:
- Automatically tracked in
computed()andeffect() - No need for
ngOnChanges— just usecomputed() - Better type safety (
input.required()vs!assertion) - Works in zoneless mode
Lazy Loading
17. How does lazy loading work in Angular routing?
Why interviewers ask it: Lazy loading is essential for production app performance. Interviewers want to see you know the modern standalone syntax.
Answer:
// app.routes.ts — modern standalone approach
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
loadComponent: () =>
import('./home/home.component').then(m => m.HomeComponent)
},
{
path: 'dashboard',
loadChildren: () =>
import('./dashboard/dashboard.routes').then(m => m.DASHBOARD_ROUTES)
},
{
path: 'admin',
canActivate: [authGuard], // combined with guard
loadChildren: () =>
import('./admin/admin.routes').then(m => m.ADMIN_ROUTES)
}
];
// dashboard/dashboard.routes.ts — child routes
export const DASHBOARD_ROUTES: Routes = [
{
path: '',
loadComponent: () =>
import('./dashboard.component').then(m => m.DashboardComponent)
},
{
path: 'analytics',
loadComponent: () =>
import('./analytics/analytics.component').then(m => m.AnalyticsComponent)
}
];Preloading strategies:
import { PreloadAllModules, withPreloading } from '@angular/router';
// main.ts
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes, withPreloading(PreloadAllModules)),
]
});
// Custom preloading strategy — preload only flagged routes
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class SelectivePreloadingStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>): Observable<any> {
return route.data?.['preload'] ? load() : of(null);
}
}
// In routes:
{ path: 'reports', data: { preload: true }, loadChildren: () => import('./reports/reports.routes') }18. What is `deferrable views` (`@defer`) in Angular 17+?
Why interviewers ask it: @defer is the newest performance tool in Angular. Asking about it filters for candidates who are current.
Answer: @defer is a template syntax for deferring component rendering until certain conditions are met — similar to React's Suspense but more powerful.
<!-- Defer loading until component is in viewport -->
@defer (on viewport) {
<app-heavy-chart [data]="chartData" />
} @placeholder {
<div class="chart-skeleton"></div>
} @loading (minimum 200ms) {
<app-spinner />
} @error {
<p>Chart failed to load.</p>
}
<!-- Defer until user interaction -->
@defer (on interaction) {
<app-comments [postId]="postId" />
} @placeholder {
<button>Load comments</button>
}
<!-- Defer on idle (after initial load, low priority) -->
@defer (on idle) {
<app-recommendations />
}
<!-- Prefetch on hover but render on click -->
@defer (on interaction; prefetch on hover) {
<app-user-profile [userId]="userId" />
}Common mistake: Using @defer for everything. It adds a tiny overhead per block. Use it for components that are below the fold, rarely used, or computationally heavy.
NgRx State Management
19. Explain the NgRx data flow: action → reducer → selector.
Why interviewers ask it: NgRx is the dominant state management library for enterprise Angular. Understanding the unidirectional data flow is the foundation.
Answer:
// 1. Define state shape
export interface UserState {
users: User[];
selectedUserId: string | null;
loading: boolean;
error: string | null;
}
// 2. Actions — describe what happened
import { createAction, props } from '@ngrx/store';
export const loadUsers = createAction('[Users Page] Load Users');
export const loadUsersSuccess = createAction(
'[Users API] Load Users Success',
props<{ users: User[] }>()
);
export const loadUsersFailure = createAction(
'[Users API] Load Users Failure',
props<{ error: string }>()
);
export const selectUser = createAction(
'[Users Page] Select User',
props<{ userId: string }>()
);
// 3. Reducer — pure function, produces new state
import { createReducer, on } from '@ngrx/store';
const initialState: UserState = {
users: [],
selectedUserId: null,
loading: false,
error: null
};
export const usersReducer = createReducer(
initialState,
on(loadUsers, state => ({ ...state, loading: true, error: null })),
on(loadUsersSuccess, (state, { users }) => ({
...state,
users,
loading: false
})),
on(loadUsersFailure, (state, { error }) => ({
...state,
loading: false,
error
})),
on(selectUser, (state, { userId }) => ({
...state,
selectedUserId: userId
}))
);
// 4. Selectors — memoized queries against state
import { createSelector, createFeatureSelector } from '@ngrx/store';
const selectUsersFeature = createFeatureSelector<UserState>('users');
export const selectAllUsers = createSelector(
selectUsersFeature,
state => state.users
);
export const selectSelectedUser = createSelector(
selectUsersFeature,
state => state.users.find(u => u.id === state.selectedUserId) ?? null
);
export const selectIsLoading = createSelector(
selectUsersFeature,
state => state.loading
);
// 5. Effects — side effects (HTTP calls)
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { inject } from '@angular/core';
export const loadUsersEffect = createEffect(
() => {
const actions$ = inject(Actions);
const userService = inject(UserService);
return actions$.pipe(
ofType(loadUsers),
switchMap(() =>
userService.getAll().pipe(
map(users => loadUsersSuccess({ users })),
catchError(error => of(loadUsersFailure({ error: error.message })))
)
)
);
}
);
// 6. Component — dispatch and select
@Component({
template: `
@if (loading$ | async) { <app-spinner /> }
@for (user of users$ | async; track user.id) {
<app-user-card [user]="user" (click)="select(user.id)" />
}
`
})
export class UsersPageComponent {
private store = inject(Store);
users$ = this.store.select(selectAllUsers);
loading$ = this.store.select(selectIsLoading);
ngOnInit() {
this.store.dispatch(loadUsers());
}
select(userId: string) {
this.store.dispatch(selectUser({ userId }));
}
}20. When should you use NgRx and when is it overkill?
Why interviewers ask it: Over-engineering is a real problem. Interviewers want pragmatic engineers, not framework zealots.
Answer: Use NgRx when:
- Multiple unrelated components need to share and sync state
- State transitions are complex (loading/error/success per entity)
- You need time-travel debugging, action log, or Redux DevTools
- The team is large and you need a strict communication contract
Skip NgRx when:
- State is local to a component or a parent-child subtree → use signals or
@Input/@Output - State is simple user data → use a service with
BehaviorSubjector signals - It's a small to medium app → services + signals + Angular Router state cover most needs
// Good enough for 80% of apps — simple signal service
@Injectable({ providedIn: 'root' })
export class CartService {
private items = signal<CartItem[]>([]);
readonly items$ = this.items.asReadonly();
readonly total = computed(() => this.items().reduce((sum, i) => sum + i.price * i.qty, 0));
addItem(item: CartItem) {
this.items.update(items => [...items, item]);
}
removeItem(id: string) {
this.items.update(items => items.filter(i => i.id !== id));
}
}Routing Guards
21. What are routing guards and how do you implement a functional guard?
Why interviewers ask it: Guards are used in every serious Angular app. The functional syntax (v14.2+) is now the standard.
Answer: Guards control navigation. Angular 14.2 deprecated class-based guards in favor of functions.
import { inject } from '@angular/core';
import { CanActivateFn, CanMatchFn, Router, ActivatedRouteSnapshot } from '@angular/router';
// Functional auth guard
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isAuthenticated()) {
return true;
}
// Store the attempted URL for redirecting after login
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url }
});
};
// Role-based guard with factory pattern
export const roleGuard = (requiredRole: string): CanActivateFn => {
return (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.hasRole(requiredRole)) return true;
return router.createUrlTree(['/unauthorized']);
};
};
// canDeactivate — prevent leaving with unsaved changes
export const unsavedChangesGuard: CanDeactivateFn<FormComponent> = (component) => {
if (component.form.dirty) {
return confirm('You have unsaved changes. Leave anyway?');
}
return true;
};
// canMatch — prevents even loading the lazy chunk
export const adminCanMatch: CanMatchFn = () => {
const auth = inject(AuthService);
return auth.hasRole('admin');
};
// Routes
export const routes: Routes = [
{
path: 'profile',
canActivate: [authGuard],
loadComponent: () => import('./profile/profile.component')
},
{
path: 'admin',
canMatch: [adminCanMatch], // more efficient — doesn't load chunk if fails
loadChildren: () => import('./admin/admin.routes')
},
{
path: 'form',
canDeactivate: [unsavedChangesGuard],
loadComponent: () => import('./form/form.component')
},
{
path: 'reports',
canActivate: [roleGuard('manager')],
loadComponent: () => import('./reports/reports.component')
}
];canActivate vs canMatch: canActivate runs after the route matches (chunk may already load). canMatch prevents the route from matching at all — use it for role-based access to lazy routes to avoid downloading code users can't use.
22. What is a `resolve` guard and when is it useful?
Why interviewers ask it: Pre-fetching data before navigation is a common pattern. Knowing when it's appropriate (vs using ngOnInit) is a senior-level distinction.
Answer: A resolve guard pre-fetches data before the component activates, ensuring the component always receives data and never needs to handle a loading state.
import { ResolveFn } from '@angular/router';
// Functional resolver
export const userResolver: ResolveFn<User> = (route) => {
const userService = inject(UserService);
const router = inject(Router);
const id = route.paramMap.get('id')!;
return userService.getUser(id).pipe(
catchError(() => {
router.navigate(['/not-found']);
return EMPTY; // prevent navigation
})
);
};
// Route
{ path: 'users/:id', resolve: { user: userResolver }, loadComponent: () => ... }
// Component — data is guaranteed to exist
@Component({ template: `<h1>{{ user.name }}</h1>` })
export class UserDetailComponent {
user = inject(ActivatedRoute).snapshot.data['user'] as User;
}When to use resolve vs ngOnInit loading: Use resolve when the component is meaningless without the data (e.g., edit form where you need the entity to populate fields). Use ngOnInit loading with a skeleton UI when the page is useful even while data loads (better perceived performance).
HTTP Interceptors
23. How do you implement an HTTP interceptor with the functional syntax?
Why interviewers ask it: Interceptors are used for auth tokens, error handling, and logging in virtually every app. The functional syntax (v15+) replaces class-based interceptors.
Answer:
import { HttpInterceptorFn, HttpRequest, HttpHandlerFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, throwError } from 'rxjs';
// Auth interceptor — adds Bearer token
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService);
const token = auth.getToken();
if (!token) return next(req);
const authReq = req.clone({
headers: req.headers.set('Authorization', `Bearer ${token}`)
});
return next(authReq);
};
// Error interceptor — global error handling
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const router = inject(Router);
const toast = inject(ToastService);
return next(req).pipe(
catchError(error => {
switch (error.status) {
case 401:
router.navigate(['/login']);
break;
case 403:
toast.error('You do not have permission for this action.');
break;
case 500:
toast.error('Server error. Please try again.');
break;
}
return throwError(() => error);
})
);
};
// Retry interceptor
import { retry } from 'rxjs/operators';
export const retryInterceptor: HttpInterceptorFn = (req, next) => {
return next(req).pipe(
retry({
count: 2,
delay: (error, attempt) => {
if (error.status === 0 || error.status >= 500) {
return timer(1000 * attempt); // 1s, 2s
}
return throwError(() => error); // don't retry 4xx
}
})
);
};
// Register in main.ts
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(
withInterceptors([authInterceptor, retryInterceptor, errorInterceptor])
)
]
});Order matters: Interceptors run in the order they're listed for the request, and in reverse order for the response. Put auth before retry, retry before error handler.
24. How would you cancel a pending HTTP request when a component is destroyed?
Why interviewers ask it: Resource cleanup — letting requests run after component destruction wastes bandwidth and can cause "set state on destroyed component" errors.
Answer:
// Option 1: takeUntilDestroyed (Angular 16+) — cleanest
@Component({ standalone: true, template: '' })
export class SearchComponent {
results: SearchResult[] = [];
constructor(
private searchService: SearchService,
private route: ActivatedRoute
) {
this.route.queryParams.pipe(
switchMap(params => this.searchService.search(params['q'])),
takeUntilDestroyed() // cancels HTTP request on destroy
).subscribe(results => this.results = results);
}
}
// Option 2: async pipe — automatic cancellation
@Component({
template: `
@for (result of results$ | async; track result.id) {
<app-result [data]="result" />
}
`
})
export class SearchComponent {
results$ = this.route.queryParams.pipe(
switchMap(params => this.searchService.search(params['q']))
); // async pipe unsubscribes = HTTP request is aborted
}Why switchMap specifically for HTTP cancellation: switchMap cancels the previous inner observable (the HTTP request) when a new outer value arrives. Combined with takeUntilDestroyed, this ensures both search-term changes and component destruction properly cancel in-flight requests.
Testing with TestBed
25. How do you set up a TestBed for a component test?
Why interviewers ask it: TestBed is the core Angular testing utility. Knowing how to configure it properly — and when to use shallow vs deep rendering — shows testing maturity.
Answer:
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { of } from 'rxjs';
describe('UserCardComponent', () => {
let component: UserCardComponent;
let fixture: ComponentFixture<UserCardComponent>;
let userServiceSpy: jasmine.SpyObj<UserService>;
beforeEach(async () => {
// Create a spy before configuring TestBed
userServiceSpy = jasmine.createSpyObj('UserService', ['getUser']);
userServiceSpy.getUser.and.returnValue(of({ id: '1', name: 'Alice', email: 'alice@test.com' }));
await TestBed.configureTestingModule({
imports: [UserCardComponent], // standalone component — use imports
providers: [
{ provide: UserService, useValue: userServiceSpy }
]
}).compileComponents();
fixture = TestBed.createComponent(UserCardComponent);
component = fixture.componentInstance;
component.userId = '1'; // set @Input before detectChanges
fixture.detectChanges(); // triggers ngOnInit
});
it('should display user name', () => {
const nameEl = fixture.debugElement.query(By.css('[data-testid="user-name"]'));
expect(nameEl.nativeElement.textContent).toContain('Alice');
});
it('should call getUser with the provided userId', () => {
expect(userServiceSpy.getUser).toHaveBeenCalledWith('1');
});
it('should emit userSelected event on click', () => {
let emittedUser: User | undefined;
component.userSelected.subscribe((u: User) => emittedUser = u);
const card = fixture.debugElement.query(By.css('.user-card'));
card.triggerEventHandler('click', null);
expect(emittedUser?.name).toBe('Alice');
});
it('should handle loading state', fakeAsync(() => {
// Reset with a delayed observable
userServiceSpy.getUser.and.returnValue(
new Observable(observer => setTimeout(() => observer.next({ id: '1', name: 'Alice', email: '' }), 500))
);
component.ngOnInit();
fixture.detectChanges();
expect(component.loading).toBeTrue();
tick(500);
fixture.detectChanges();
expect(component.loading).toBeFalse();
}));
});26. How do you test a service that makes HTTP calls?
Why interviewers ask it: HTTP service testing with HttpTestingController is a fundamental skill. Getting this right prevents tests that make real HTTP calls.
Answer:
import { TestBed } from '@angular/core/testing';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { provideHttpClient } from '@angular/common/http';
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
UserService,
provideHttpClient(),
provideHttpClientTesting() // replaces real HTTP with mock
]
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify(); // ensures no unexpected requests were made
});
it('should fetch a user by id', () => {
const mockUser: User = { id: '1', name: 'Alice', email: 'alice@test.com' };
service.getUser('1').subscribe(user => {
expect(user).toEqual(mockUser);
});
const req = httpMock.expectOne('/api/users/1');
expect(req.request.method).toBe('GET');
req.flush(mockUser); // respond with mock data
});
it('should include auth header', () => {
service.getUser('1').subscribe();
const req = httpMock.expectOne('/api/users/1');
expect(req.request.headers.get('Authorization')).toBeTruthy();
req.flush({});
});
it('should handle 404', () => {
service.getUser('999').subscribe({
error: err => expect(err.status).toBe(404)
});
const req = httpMock.expectOne('/api/users/999');
req.flush('Not found', { status: 404, statusText: 'Not Found' });
});
});27. What is `fakeAsync` and `tick` and when do you need them?
Why interviewers ask it: Many Angular tests involve timing (debounce, setTimeout, animations). fakeAsync is how you control time in tests.
Answer:
import { fakeAsync, tick, discardPeriodicTasks } from '@angular/core/testing';
describe('SearchComponent', () => {
it('should debounce search input by 300ms', fakeAsync(() => {
const searchSpy = spyOn(service, 'search').and.returnValue(of([]));
// Simulate rapid typing
component.searchTerm = 'a';
component.onInput();
component.searchTerm = 'an';
component.onInput();
component.searchTerm = 'ang';
component.onInput();
expect(searchSpy).not.toHaveBeenCalled(); // debounce hasn't fired yet
tick(300); // advance virtual time by 300ms
expect(searchSpy).toHaveBeenCalledTimes(1);
expect(searchSpy).toHaveBeenCalledWith('ang');
}));
it('should poll every 5 seconds', fakeAsync(() => {
const pollSpy = spyOn(service, 'poll').and.returnValue(of(null));
component.startPolling();
tick(5000);
expect(pollSpy).toHaveBeenCalledTimes(1);
tick(5000);
expect(pollSpy).toHaveBeenCalledTimes(2);
discardPeriodicTasks(); // clean up interval, or test will fail
}));
});tick() vs flushMicrotasks(): tick(ms) advances the virtual clock. flushMicrotasks() only flushes Promise/microtask queue without advancing the clock. Use tick(0) to flush microtasks AND pending setTimeout(0).
More Essential Questions
28. What is the `async` pipe and why is it preferred over manual subscription in templates?
Answer:
// Manual subscription — problematic
@Component({
template: `<p>{{ user?.name }}</p>`
})
export class BadComponent implements OnInit, OnDestroy {
user: User | null = null;
private sub!: Subscription;
ngOnInit() { this.sub = this.userService.user$.subscribe(u => this.user = u); }
ngOnDestroy() { this.sub.unsubscribe(); }
}
// async pipe — preferred
@Component({
template: `
@if (user$ | async; as user) {
<p>{{ user.name }}</p>
}
`
})
export class GoodComponent {
user$ = inject(UserService).user$; // no lifecycle hooks needed
}The async pipe: subscribes automatically, unsubscribes on component destruction, triggers OnPush change detection when the observable emits, and handles null/undefined safely with @if.
29. What are standalone components and why did Angular introduce them?
Answer:
// Before standalone — required NgModule declaration
@NgModule({
declarations: [UserCardComponent],
imports: [CommonModule, RouterModule],
exports: [UserCardComponent]
})
export class UserCardModule {}
// Standalone — no NgModule needed
@Component({
selector: 'app-user-card',
standalone: true,
imports: [CommonModule, RouterLink, AsyncPipe], // direct imports
template: `
<a [routerLink]="['/users', user.id]">{{ user.name }}</a>
`
})
export class UserCardComponent {
@Input() user!: User;
}NgModules created indirection, boilerplate, and made tree-shaking harder. Standalone components import exactly what they use, making code more explicit, reducing bundle size, and simplifying testing (no module to configure).
30. How does Angular's `trackBy` improve `@for` loop performance?
Answer:
// Without trackBy — re-renders ALL items on any change
@Component({
template: `
<!-- Old syntax without trackBy -->
<div *ngFor="let item of items">{{ item.name }}</div>
<!-- New @for with required track expression -->
@for (item of items; track item.id) {
<app-item [item]="item" />
}
`
})
export class ListComponent {
items: Item[] = [];
refresh() {
this.items = this.items.map(i => ({ ...i, timestamp: Date.now() }));
// @for with track item.id: only changed items re-render
// *ngFor without trackBy: ALL items re-render because new array
}
}The new @for syntax requires a track expression (won't compile without it), which Angular uses to identify DOM nodes. Tracked by id means Angular patches only changed items instead of destroying and recreating the entire list.
31. What is the difference between `providedIn: 'root'`, `providedIn: 'platform'`, and `providedIn: 'any'`?
Answer:
// providedIn: 'root' — single instance for the entire app (most common)
@Injectable({ providedIn: 'root' })
export class AppSingletonService {}
// providedIn: 'platform' — shared across multiple Angular apps on the same page
// Use case: micro-frontends sharing a service instance
@Injectable({ providedIn: 'platform' })
export class SharedAnalyticsService {}
// providedIn: 'any' — one instance per lazy-loaded module + one for eager modules
// Each lazy chunk gets its own instance
@Injectable({ providedIn: 'any' })
export class ModuleIsolatedService {}For standalone apps, 'root' covers 95% of cases. 'platform' is for micro-frontend architectures.
32. Explain `ng-content`, `ng-template`, `ng-container` and when to use each.
Answer:
// ng-content — projects content from parent into child
@Component({
selector: 'app-card',
template: `
<div class="card">
<div class="header">
<ng-content select="[slot=header]"></ng-content>
</div>
<div class="body">
<ng-content></ng-content>
</div>
</div>
`
})
export class CardComponent {}
// Usage
<app-card>
<h2 slot="header">Title</h2>
<p>Body content</p>
</app-card>
// ng-template — define a template fragment without rendering it
// Use with structural directives or as a fallback
@Component({
template: `
@if (loading) {
<ng-container *ngTemplateOutlet="skeletonTpl"></ng-container>
} @else {
<p>{{ data }}</p>
}
<ng-template #skeletonTpl>
<div class="skeleton skeleton-text"></div>
</ng-template>
`
})
export class DataComponent {}
// ng-container — grouping without adding DOM node
// Perfect for applying multiple structural directives
@Component({
template: `
<!-- Avoids extra div in DOM -->
<ng-container *ngIf="isAdmin">
<button>Edit</button>
<button>Delete</button>
</ng-container>
`
})
export class ActionBarComponent {}33. What is `HostBinding` and `HostListener` and when would you use them?
Answer:
import { Directive, HostBinding, HostListener, Input } from '@angular/core';
@Directive({
selector: '[appTooltip]',
standalone: true
})
export class TooltipDirective {
@Input() appTooltip = '';
@HostBinding('attr.title') get title() {
return this.appTooltip;
}
@HostBinding('class.tooltip-active') isActive = false;
@HostBinding('style.cursor') cursor = 'help';
@HostListener('mouseenter') show() {
this.isActive = true;
}
@HostListener('mouseleave') hide() {
this.isActive = false;
}
@HostListener('window:scroll') onScroll() {
this.isActive = false; // hide tooltip on scroll
}
}
// host property in @Component (preferred in modern Angular)
@Component({
selector: 'app-button',
host: {
'class': 'btn',
'[class.btn-primary]': 'variant === "primary"',
'[disabled]': 'disabled',
'(click)': 'onClick($event)'
},
template: `<ng-content></ng-content>`
})
export class ButtonComponent {
@Input() variant = 'default';
@Input() disabled = false;
onClick(event: MouseEvent) {
if (this.disabled) event.stopPropagation();
}
}34. What is the `@defer` block's `when` trigger vs the `on` triggers?
Answer:
@Component({
template: `
<!-- on: built-in triggers (viewport, idle, interaction, hover, timer, immediate) -->
@defer (on viewport) {
<app-chart />
}
<!-- when: custom condition — a boolean expression -->
@defer (when dataLoaded && userIsAdmin) {
<app-admin-panel />
}
<!-- Combine: defer when condition, but prefetch on idle -->
@defer (when isExpanded; prefetch on idle) {
<app-details [data]="item" />
} @placeholder {
<button (click)="isExpanded = true">Show details</button>
}
`
})
export class ExampleComponent {
dataLoaded = false;
userIsAdmin = false;
isExpanded = false;
}on triggers are events Angular detects automatically (intersection, user event). when is a condition Angular evaluates reactively — the block loads as soon as the expression becomes true.
35. How do you share data between sibling components without a shared parent?
Answer:
// Option 1: Shared service with signals (simplest)
@Injectable({ providedIn: 'root' })
export class SharedStateService {
private _selectedTab = signal<string>('home');
readonly selectedTab = this._selectedTab.asReadonly();
setTab(tab: string) {
this._selectedTab.set(tab);
}
}
// SiblingA dispatches
@Component({ selector: 'app-sidebar', standalone: true })
export class SidebarComponent {
state = inject(SharedStateService);
selectTab(tab: string) { this.state.setTab(tab); }
}
// SiblingB reacts
@Component({
selector: 'app-content',
standalone: true,
template: `<div>Active tab: {{ state.selectedTab() }}</div>`
})
export class ContentComponent {
state = inject(SharedStateService);
}
// Option 2: NgRx for complex apps
// Dispatch an action from SiblingA, select state in SiblingB
// Option 3: RxJS EventBus (rare, use service + Subject instead)
@Injectable({ providedIn: 'root' })
export class EventBusService {
private events$ = new Subject<{ type: string; payload: any }>();
readonly on$ = this.events$.asObservable();
emit(type: string, payload: any) { this.events$.next({ type, payload }); }
}36. What is Angular's `DestroyRef` and how does it replace `ngOnDestroy`?
Answer:
import { DestroyRef, inject } from '@angular/core';
// DestroyRef — provides a way to register cleanup callbacks
@Injectable() // can be used in services too, not just components
export class ModernService {
constructor() {
const destroyRef = inject(DestroyRef);
const interval = setInterval(() => this.poll(), 5000);
// Register cleanup — runs when the injector is destroyed
destroyRef.onDestroy(() => {
clearInterval(interval);
});
}
private poll() { /* ... */ }
}
// takeUntilDestroyed uses DestroyRef internally
@Component({ standalone: true, template: '' })
export class CleanComponent {
constructor() {
this.dataService.data$
.pipe(takeUntilDestroyed()) // reads DestroyRef from current injection context
.subscribe(data => console.log(data));
}
}DestroyRef allows cleanup to be registered anywhere in the injection context, not just in ngOnDestroy. This is what powers takeUntilDestroyed() and enables cleanup logic in services, directives, and helper functions.
37. How do you handle environment-specific configuration in Angular?
Answer:
// environments/environment.ts (development)
export const environment = {
production: false,
apiUrl: 'http://localhost:3000',
featureFlags: { newDashboard: true, betaSearch: false }
};
// environments/environment.prod.ts
export const environment = {
production: true,
apiUrl: 'https://api.myapp.com',
featureFlags: { newDashboard: false, betaSearch: false }
};
// Modern approach — injection token (testable, flexible)
export const ENVIRONMENT = new InjectionToken<typeof environment>('environment');
// main.ts
bootstrapApplication(AppComponent, {
providers: [
{ provide: ENVIRONMENT, useValue: environment }
]
});
// Usage in service
@Injectable({ providedIn: 'root' })
export class ApiService {
private env = inject(ENVIRONMENT);
private http = inject(HttpClient);
get<T>(path: string) {
return this.http.get<T>(`${this.env.apiUrl}${path}`);
}
}38. What is the Angular CDK and name three things you'd use it for?
Answer: The Angular CDK (Component Dev Kit) is a set of primitives for building UI components without styling opinions. Three common uses:
- 1Virtual scrolling — render only visible items in large lists
import { ScrollingModule } from '@angular/cdk/scrolling';
@Component({
template: `
<cdk-virtual-scroll-viewport itemSize="50" style="height: 400px">
<div *cdkVirtualFor="let item of items">{{ item.name }}</div>
</cdk-virtual-scroll-viewport>
`
})
export class LargeListComponent {
items = Array.from({ length: 10000 }, (_, i) => ({ name: `Item ${i}` }));
}- 2Drag and drop — reorderable lists
- 3Overlay/Portal — modals, tooltips positioned relative to elements with proper z-index and scroll handling
39. How do you implement a custom form control with `ControlValueAccessor`?
Answer:
import { Component, forwardRef, signal } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-star-rating',
standalone: true,
imports: [NgFor],
template: `
<div class="stars">
@for (star of stars; track star) {
<button
[class.filled]="star <= value()"
(click)="select(star)"
[disabled]="disabled()"
type="button">★</button>
}
</div>
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => StarRatingComponent),
multi: true
}
]
})
export class StarRatingComponent implements ControlValueAccessor {
stars = [1, 2, 3, 4, 5];
value = signal(0);
disabled = signal(false);
private onChange: (value: number) => void = () => {};
private onTouched: () => void = () => {};
// Called when Angular sets the form value
writeValue(value: number): void {
this.value.set(value ?? 0);
}
// Register the callback Angular will call when value changes
registerOnChange(fn: (value: number) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled.set(isDisabled);
}
select(star: number): void {
this.value.set(star);
this.onChange(star); // notify Angular forms
this.onTouched();
}
}
// Usage in a reactive form — works like any native input
@Component({
template: `
<form [formGroup]="form">
<app-star-rating formControlName="rating"></app-star-rating>
</form>
`
})
export class ReviewFormComponent {
form = new FormGroup({
rating: new FormControl(0, Validators.min(1))
});
}40. How do you profile and optimize a slow Angular application?
Answer: A structured approach:
1. Use Angular DevTools profiler to identify which components are re-rendering unnecessarily and how long each cycle takes.
2. Apply OnPush aggressively — the highest-impact change for render performance.
3. Check for unnecessary re-renders with ng-what-changed:
// Temporarily log what changed in a component
@Component({ changeDetection: ChangeDetectionStrategy.OnPush })
export class ListComponent implements DoCheck {
ngDoCheck() {
console.log('Checking ListComponent'); // if this fires too often, something is wrong
}
}4. Use trackBy (or track in @for) for all lists.
5. Audit bundle size with source-map-explorer:
ng build --source-map
npx source-map-explorer dist/app/*.js6. Lazy load routes aggressively — no non-critical code in the initial bundle.
7. Use @defer for below-the-fold content.
8. Debounce user input handlers:
// Don't: triggers on every keystroke
<input (input)="search($event.target.value)" />
// Do: debounce in the stream
searchTerm$ = new Subject<string>();
ngOnInit() {
this.searchTerm$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term => this.searchService.search(term))
).subscribe(results => this.results = results);
}9. For large lists, consider CDK virtual scrolling instead of rendering all items.
10. Check for accidental ChangeDetectorRef.detectChanges() in hot paths — calling this in an ngDoCheck or a rapid event handler kills performance.
What to Prepare Beyond These Questions
Interviewers often pair these questions with a coding challenge: implement a data table with sorting and pagination, build a multi-step form with validation and state persistence, or refactor a component from Default to OnPush change detection and fix the resulting bugs. Practice writing Angular code without autocomplete — understanding why each piece is there is more valuable than memorizing syntax.
The candidates who get offers can articulate the tradeoffs: why OnPush over Default, when signals replace RxJS (and when they don't), why canMatch over canActivate for role-based lazy routes. Know the why, and the code follows.