InterviewHack.ai
Empezar gratis
Blog/Angular Developer Interview Questions — 40 with Code and Answers

Angular Developer Interview Questions — 40 with Code and Answers

September 16, 2026

angulartypescript

The 40 Angular interview questions that actually get asked at real companies: dependency injection, RxJS, change detection, lazy loading, signals. Detailed answers with TypeScript code.

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).

typescript
// 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. 1constructor — dependency injection, nothing else
  2. 2ngOnChanges — fires when @Input() values change (before ngOnInit on first run)
  3. 3ngOnInit — component initialized, inputs available
  4. 4ngDoCheck — every change detection cycle
  5. 5ngAfterContentInit — projected content () initialized
  6. 6ngAfterContentChecked — after each check of projected content
  7. 7ngAfterViewInit — component's view and child views initialized
  8. 8ngAfterViewChecked — after each check of the view
  9. 9ngOnDestroy — cleanup before component is removed
typescript
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.
typescript
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 .
typescript
// 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:

typescript
// 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:

typescript
// 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.

typescript
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).

typescript
// 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:

typescript
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: 3

Real-world usage:

  • BehaviorSubject — auth state, user profile, theme, any "current state" service
  • ReplaySubject(1) — when you want BehaviorSubject behavior but can't provide an initial value
  • Subject — event bus, component communication where you don't need history
  • AsyncSubject — 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.

typescript
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 new
  • merge = run everything at once
  • concat = queue and wait
  • exhaust = 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.

typescript
@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:

typescript
// 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:
  1. 1An @Input() reference changes (not just mutation)
  2. 2An event originates from the component or its children
  3. 3An observable via async pipe emits
  4. 4You manually call markForCheck() or detectChanges()
typescript
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):

typescript
// 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.

typescript
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:

typescript
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 emits
  • requireSync — throws if observable doesn't emit synchronously (useful with BehaviorSubject)
  • 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:

typescript
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() and effect()
  • No need for ngOnChanges — just use computed()
  • 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:

typescript
// 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:

typescript
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.

html
<!-- 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:

typescript
// 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 BehaviorSubject or signals
  • It's a small to medium app → services + signals + Angular Router state cover most needs
typescript
// 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.

typescript
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.

typescript
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:

typescript
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:

typescript
// 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:

typescript
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:

typescript
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:

typescript
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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
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:

typescript
@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:

typescript
// 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:

typescript
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:

typescript
// 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:

  1. 1Virtual scrolling — render only visible items in large lists
typescript
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}` }));
}
  1. 2Drag and drop — reorderable lists
  2. 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:

typescript
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:

typescript
// 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:

bash
ng build --source-map
npx source-map-explorer dist/app/*.js

6. 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:

typescript
// 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.

FAQ

What Angular version do most companies use in interviews?+

Most companies are on Angular 15-17 for active projects, but interview questions cover concepts that span versions. Knowing standalone components, functional guards, and signals (v17+) signals seniority. Classic NgModule knowledge is still relevant for legacy codebases — don't skip it.

How long does a typical Angular technical interview take?+

Usually 60-90 minutes split into a conceptual portion (questions like these) and a live coding or take-home challenge. Senior roles often add a system design discussion where you architect a feature end-to-end including state management and API integration.

What is the most common mistake Angular developers make in interviews?+

Confusing observables with promises and treating RxJS operators interchangeably. Specifically, using mergeMap when they need switchMap (causes race conditions) and not understanding the difference between shareReplay and BehaviorSubject for caching. Practice explaining why switchMap cancels previous requests.

Do I need to know NgRx for Angular interviews?+

For senior roles at larger companies, yes. Know the action-reducer-selector-effect data flow and be able to explain when NgRx adds value versus when a signal service is enough. Many interviewers appreciate knowing both and being able to justify the choice.

How important are Angular Signals for a 2024/2025 interview?+

Very important for any role targeting Angular 17+. Expect questions on signal(), computed(), effect(), input() signal inputs, and the toSignal()/toObservable() bridges. Understanding why signals improve performance over zone-based change detection is a strong differentiator.

What is the best way to practice Angular interview questions?+

Build a small project that hits every topic: lazy-loaded routes with guards, an HTTP service with interceptors and error handling, a component with OnPush change detection and RxJS operators, and at least one custom form control with ControlValueAccessor. Write tests for each piece using TestBed. Hands-on practice beats reading documentation by a wide margin.

Artículos relacionados

TypeScript Interview Questions and How to Answer Them (45+ Questions)

A comprehensive, code-backed reference covering 47 TypeScript interview questions across beginner, intermediate, and senior levels. Topics include type system fundamentals, structural typing, generics, conditional types, infer, mapped types, template literal types, utility types, discriminated unions, covariance/contravariance, branded types, recursive types, decorators, and real-world design patterns.

How to Land a Remote TypeScript Job Paying USD From LATAM

Learn how to secure a TypeScript remote job paid in USD from Latin America: skill benchmarks, application tactics, interview prep, and payment setup.

Preparate para tu entrevista real

Pegá el link de tu vacante: investigamos quién te entrevista y te ensayamos en vivo.

Empezar gratis →

¿Tenés entrevista próxima? Instalá el copiloto en vivo →

InterviewHack.ai

Preparate para la entrevista exacta: quién te entrevista, tu CV a medida y coach real.

Producto

VacantesRevisar CV (ATS) gratis¿Cómo suena tu inglés?¿Te pagan bien?Reporte de sueldos LATAMCursos gratisBlogCV a medidaPráctica habladaEs gratis

Empleos remotos

ReactPythonFull-StackLATAMArgentinaMéxicoVer todas →

Preparate

Práctica habladaFrontendBackendAI EngineerPor empresaVendete con tu CV

Empresa

Buscás talentoAcerca deContactoPrivacidadTérminos

© 2026 InterviewHack.ai · Tu CV es tuyo. Nunca se usa para entrenar nada. · Un producto de IA-PTY