Ch. 04

Angular interview questions & answers

Components, dependency injection, change detection, signals and the RxJS operators that come up in every Angular round.

112 interview questions32 quiz questions24 notes
your progress0%

Notes in this chapter

Filter all notes →
read ✓Angular · hard

Angular Global Error Handling

What actually reaches Angular's ErrorHandler, how to write one that never throws or loops, and where HTTP and chunk-load errors belong.

~6 min readread →
read ✓Angular · hard

Angular Resource API for Async Data

How resource() turns a signal-driven request into value, status and error signals, cancels stale loads, and why value() can throw on error.

~6 min readread →
read ✓Angular · hard

Angular Zoneless Change Detection

Without zone.js, Angular renders only when notified. See which updates notify, which silently don't, and how TestBed catches the gaps for you.

~6 min readread →
read ✓Angular · mid

Angular Async Validators and Pending State

A username-availability check traced step by step: PENDING status, cancelled checks, debouncing, errors that hang the form, and safe submit.

~6 min readread →
read ✓Angular · hard

Angular Component Tests That Verify Behavior

Test an Angular component through its DOM, inputs, outputs and HTTP calls, so refactors pass and real bugs fail. Five tests, run and broken.

~6 min readread →
read ✓Angular · easy

Angular OnPush and Input Reference Changes

Why an OnPush child ignores user.role = 'admin' but updates for a new object, what else marks it dirty, and how signal inputs change the picture.

~6 min readread →
read ✓Angular · mid

Angular Route Guards and Server Authorization

Route guards decide what the Angular router shows, not what a user may do. Why the admin chunk is still public and where the real check belongs.

~7 min readread →
read ✓Angular · mid

Angular Resolvers and Navigation Data

What a ResolveFn really does to navigation: the delay, reruns on param change, RedirectCommand for 404s, and why EMPTY or an error cancels it.

~6 min readread →
read ✓Angular · easy

Angular Computed Signals and Derived Values

Why a cart total belongs in computed() and not a second signal, how Angular tracks dependencies lazily, and when linkedSignal fits.

~7 min readread →

112 Angular interview questions study by subtopic

112 questions
  1. 1.How would you design a reusable selection component, such as a custom select or chip picker, in Angular?mid

    I'd start with the inputs. Options arrive through options = input.required<readonly Option[]>(), each with a stable id and a separate label, so display text can change or be translated without breaking stored values. The selection is value = model<string | null>(null), which gives the parent [(value)]="countryId". For reactive forms I'd also implement ControlValueAccessor, or build on the CDK's cdkListbox, which already handles arrow keys, typeahead and forms.

    Compare options by id, never by object reference. After a refetch the server returns new objects, and a === check silently drops the selection; that's why mat-select has compareWith.

    It also needs disabled options, an empty state, and role="listbox" with aria-selected. And it must never sort or splice the options array in place, because that array belongs to the parent. Derive a sorted copy with computed() and send changes out through the model.

    What interviewers listen for
    • input.required() for options, model() for the value
    • Stable id kept separate from the display label
    • Compare by id, as compareWith does, not by reference
    • Listbox roles plus disabled and empty states
    • Never mutate the parent's options array

    Likely follow-up: When would you implement ControlValueAccessor instead of relying on a model() input? · How would you add multiple selection without breaking the single-select API?

  2. 2.A standalone Angular screen has grown to an 800-line template. How would you split it?mid

    I'd split it along what changes together. Take an order details page. The page component stays as the container: it injects the store, reads the route id, loads data and handles save and cancel. Under it sit presentational components such as OrderSummary, LineItemsTable and ShippingForm. They get data through input() and report user actions through output(), for example (quantityChange)="store.updateQuantity($event)", and they never call services themselves, which makes each one easy to test with plain inputs.

    Each standalone component lists only what its own template uses in imports, such as CurrencyPipe or RouterLink. I wouldn't add a SharedModule that exports forty things, because then nobody can tell what a component actually depends on.

    Two more tools help: @defer (on viewport) for heavy sections lower on the page, and child routes when the screen is really several tabs. The mistake to avoid is splitting by line count, which produces components that need ten inputs and still share hidden state.

    What interviewers listen for
    • Container injects services; children use input() and output()
    • Split by what changes together, not by line count
    • Each component imports only what its template uses
    • No catch-all SharedModule
    • @defer or child routes for heavy or tabbed sections

    Likely follow-up: How would you share one reactive form between the container and a child component?

  3. 3.Editable rows rendered by this @for loop lose what the user typed whenever the list refreshes from the server. What would you check?mid

    First, the track expression. track row keys each row by object reference. A refresh brings new objects, so every key looks new: Angular destroys each app-row-editor and creates a fresh one, and anything held inside it, such as a half-typed value, focus or an open dropdown, is gone. The fix is track row.id, a stable key from the data. track $index is wrong too: it keeps rows by position, so after a sort or a deletion the typed value stays in row 3 while a different record moves into row 3.

    Second, where the edits live. Unsaved edits shouldn't exist only in a component that can be destroyed. I'd keep them in a FormArray or a Map keyed by id, and merge fresh data without overwriting rows the user has touched.

    Then I'd test it: start typing in a row, trigger a refresh, insert a row above it, sort, and check the value stays with the right record.

    @for (row of rows(); track row) {
      <app-row-editor [row]="row" />
    }
    What interviewers listen for
    • track row keys by reference, so a refresh recreates rows
    • Use track row.id, not $index
    • Keep unsaved edits in a form or map keyed by id
    • Merge refreshed data without overwriting touched rows
    • Test refresh, insert, delete and sort while editing

    Likely follow-up: If the track key matches but the object reference changed, what does Angular do with that row?

  4. 4.When should a complex template expression move into the component class?mid

    When it encodes a business rule, repeats, or does real work. A condition like @if (user().roles.includes('admin') && !user().suspended && order().total > 1000) is a refund policy hiding in markup. You can't unit test it without rendering, and the next developer copies it into another template. I'd name it in the class, canRefund = computed(() => ...), and write @if (canRefund()).

    Performance is the other reason. A method call such as {{ formatTotal(order) }} runs on every change detection pass that checks the component, which can be many times per second with the default strategy. A computed() signal or a pure pipe recalculates only when its inputs change. Calling a signal like total() in a template is fine, though: it's a cheap read of a cached value, not a recalculation.

    Simple bindings like [disabled]="saving()" stay in the template. For repetition that isn't logic, @let (stable since Angular 19) gives a local alias, for example @let address = order().shipping.address;.

    What interviewers listen for
    • Name business rules as computed() signals
    • Template method calls run on every check
    • computed() and pure pipes cache results
    • Reading a signal like total() is cheap
    • @let for local aliases, not logic

    Likely follow-up: Why is calling total() in a template different from calling getTotal()?

  5. 5.What does a reusable click-outside directive need to handle beyond checking contains(event.target)?mid

    The basic version listens on the document and emits when host.contains(event.target) is false. Real apps add several traps:

    • Overlays. A CDK overlay or mat-menu opened from inside the panel renders under body, so a click in that menu looks like an outside click. Accept extra elements that count as inside, or use the CDK's overlayOutsideClick output when the panel is itself a connected overlay.
    • The opening click. If the listener is attached while the opening click is still bubbling, that same click reaches the document and closes the panel. Listening to pointerdown instead of click avoids it, and also stops a text selection that ends outside from closing anything.
    • Shadow DOM. event.target is retargeted to the shadow host, so check event.composedPath().
    • Cleanup. A host listener like '(document:pointerdown)' is removed automatically; a manual addEventListener needs DestroyRef.onDestroy.
    • Keyboard. Escape and focus leaving the panel should close it too.

    I'd test nested popovers and keyboard dismissal, not just one mouse click.

    What interviewers listen for
    • Overlays render under body; allow extra inside elements
    • pointerdown so the opening click doesn't close it
    • composedPath() for Shadow DOM
    • Host listeners clean up; manual ones need DestroyRef
    • Escape and focus loss close it too

    Likely follow-up: In a zone.js app, how would you stop a document-level listener from triggering change detection on every click?

  6. 6.How do you choose between a pure pipe and a computed() signal for derived display data?mid

    I ask whether the transformation is generic formatting or part of one component's state.

    A pure pipe suits reusable formatting that doesn't belong to any one component: {{ amount | money: currency }} or {{ createdAt | relativeTime }}. It reruns only when its input or an argument changes, by reference for objects, and any template can use it.

    A computed() suits derived state, such as visibleTodos = computed(() => this.todos().filter(t => this.filter() === 'all' || t.status === this.filter())). It's memoized, several template bindings can read it, and the class can build on it too, for example canClear = computed(() => this.visibleTodos().length > 0). A pipe is awkward to reuse from TypeScript.

    Both rely on immutable updates. A pure pipe won't rerun if you push into the same array, and a signal won't notify anyone if update() returns the same mutated array, because signals compare values with Object.is. Neither tool fixes mutation.

    What interviewers listen for
    • Pipe for reusable formatting across templates
    • computed() for component or store state
    • The class can reuse a computed(); pipes are awkward there
    • Both need immutable updates
    • Signals compare with Object.is

    Likely follow-up: When would you mark a pipe pure: false, and what does it cost?

  7. 7.How would you investigate an ExpressionChangedAfterItHasBeenCheckedError (NG0100) in development?hard

    I'd start with the message. It names the expression and both values, such as Previous value for 'title': 'Loading'. Current value: 'Loaded'. The error exists only in development: after each change detection pass, Angular runs a second, read-only checkNoChanges pass and throws if a binding moved in between. Production skips that pass, so the bug is still there, just unreported.

    The usual causes: most often a child writes state the parent already rendered, such as a child's ngAfterViewInit calling layout.setTitle(). Next comes an expression that differs on every read, like Date.now() in a getter, and then feedback loops between parent and child bindings.

    The fix is moving where the value is produced: compute it in the parent, set it earlier in the constructor or ngOnInit, or hold it in a signal, because a signal write marks the reading view for another refresh in the same tick. Wrapping the write in setTimeout or calling detectChanges() only hides it, and zoneless, the value may never render.

    What interviewers listen for
    • Dev-only checkNoChanges pass compares every binding
    • A child's ngAfterViewInit writing parent state is the usual cause
    • Non-deterministic expressions like Date.now()
    • Move the write earlier or use a signal
    • setTimeout and detectChanges() only hide it

    Likely follow-up: Why can an OnPush tree hide this error, and how would you find it anyway?

  8. 8.How would you safely measure an element that's only rendered inside an @if block?mid

    The element doesn't exist until the @if turns true, so @ViewChild read in ngAfterViewInit is the wrong tool: that hook runs once, possibly before the panel appears. A signal query, panel = viewChild<ElementRef<HTMLElement>>('panel'), is undefined while the block is hidden and holds the element once it renders.

    I read layout in afterRenderEffect. It runs after Angular has updated the DOM, only in the browser, and again whenever panel() changes. That covers server rendering too: render hooks never run on the server, where there's no layout to measure, so no isPlatformBrowser checks are needed.

    If the size can change later, I attach a ResizeObserver and disconnect it in onCleanup, which runs when the element goes away and when the component is destroyed. If I also write styles, the earlyRead and write phases keep all reads before writes, which avoids forced synchronous layout. What I avoid is setTimeout guesses about when the element exists.

    panel = viewChild<ElementRef<HTMLElement>>('panel');
    panelHeight = signal(0);
    
    constructor() {
      afterRenderEffect((onCleanup) => {
        const el = this.panel()?.nativeElement;
        if (!el) return;
        const observer = new ResizeObserver(([entry]) =>
          this.panelHeight.set(entry.contentRect.height),
        );
        observer.observe(el);
        onCleanup(() => observer.disconnect());
      });
    }
    What interviewers listen for
    • viewChild() is undefined until the @if renders
    • Read layout in afterRenderEffect or afterNextRender
    • Render hooks don't run on the server
    • Disconnect the ResizeObserver in onCleanup
    • Batch DOM reads before writes

    Likely follow-up: What's the difference between afterNextRender and afterRenderEffect?

  9. 9.Two sibling widgets unexpectedly keep separate copies of a service's state. What would you check?mid

    Where the service is provided, because every injector that lists a provider creates its own instance. The usual culprit is providers: [CartService] in a component decorator. Each instance of that component gets a new CartService, which shadows the root singleton for that component and its children, even though the class says providedIn: 'root'. Two other common causes are a lazy route's providers array, which creates a separate instance for everything under that route, and importProvidersFrom(SomeModule) providing it again.

    To confirm, I'd open the injector tree in Angular DevTools, or log an instance id in the constructor and see two values.

    Then decide what the state should be. A cart shared across the page belongs at root, so I'd delete the component provider. Per-widget state, like an unsaved draft in each editor, is a good reason to keep component providers, and Angular destroys that instance with the component. A test that renders both widgets should check that an update in one shows in the other.

    What interviewers listen for
    • Every injector with a provider creates its own instance
    • Component providers shadow the root singleton
    • Lazy route providers create another instance
    • Check the DevTools injector tree or log an instance id
    • Shared state at root; per-widget state in component providers

    Likely follow-up: What's the difference between providers and viewProviders on a component?

  10. 10.How would you replace a service with a fake in an Angular component test?mid

    Usually through TestBed: TestBed.configureTestingModule({ imports: [OrderPage], providers: [{ provide: OrderApi, useValue: fakeApi }] }). The fake has the same public methods and returns controlled data, such as of(order) for success or throwError(() => new HttpErrorResponse({ status: 500 })) for the failure path. If the real service exposes signals, the fake should use real signal() values so the template reacts.

    The trap is a component that declares its own provider, @Component({ providers: [OrderStore] }). That provider sits on the component's element injector, which is closer than TestBed's, so your fake is ignored and the real store runs. Replace it with TestBed.overrideComponent(OrderPage, { set: { providers: [{ provide: OrderStore, useValue: fakeStore }] } }) or TestBed.overrideProvider(OrderStore, { useValue: fakeStore }).

    Then assert on what the user sees: the error message renders, and the retry button calls the fake again. If the service is a thin wrapper over HTTP, I often keep it real and fake the backend with HttpTestingController instead.

    What interviewers listen for
    • TestBed providers with useValue: fake
    • Component-level providers beat TestBed providers
    • overrideComponent or overrideProvider replaces them
    • Fake success and failure, such as throwError()
    • Assert rendered output, not private fields

    Likely follow-up: When would you keep the real service and use HttpTestingController instead?

  11. 11.When should computed() replace an effect() that copies one signal into another, as in this snippet?mid

    Whenever the second value is purely derived, as fullName is here. The writable signal and the effect both go, replaced by fullName = computed(() => this.first() + ' ' + this.last()).

    The effect version has real costs. Effects run later, during change detection, so code that sets first and immediately reads fullName gets the old name. It's also a second writable source: anyone can call fullName.set() and the two drift apart. And it does extra work for nothing. Since Angular 19, writing signals inside effects is allowed by default, so nothing warns you; it's simply the wrong tool.

    Effects are for side effects that leave the signal graph: saving to localStorage, logging, or driving a chart library. When a value is derived but the user can override it, such as a default shipping method that resets when the country changes, linkedSignal() fits better.

    first = signal('Ada');
    last = signal('Lovelace');
    fullName = signal('');
    
    constructor() {
      effect(() => this.fullName.set(`${this.first()} ${this.last()}`));
    }
    What interviewers listen for
    • Derived values belong in computed()
    • Effects run later, so the copy is briefly stale
    • A second writable signal can drift
    • Effects for side effects like localStorage or charts
    • linkedSignal() for derived values the user can override

    Likely follow-up: How does linkedSignal() differ from computed() plus a separate override signal?

  12. 12.How would you build a signal-driven details view that loads a record whenever the selected ID changes?hard

    I'd derive the request from the selection and let a resource handle the rest. With httpResource (added in 19.2, experimental in 19 and 20) the request function reads orderId() and returns a URL, or undefined when nothing is selected. Returning undefined keeps the resource idle, so there's no request to /api/orders/undefined.

    When the id changes, the resource aborts the in-flight request and starts a new one, so a slow response for order 41 can't overwrite order 42. During that load value() drops to undefined and isLoading() is true; a manual reload(), by contrast, keeps the current value on screen while it refreshes. In v20, reading value() in the error state throws, so the template guards with hasValue() and offers a retry that calls reload().

    Without resources, the same design is toObservable(orderId) piped through switchMap to HttpClient, then toSignal(). I'd test with HttpTestingController: select 41, then 42, check the first request was cancelled and only 42 renders.

    @Component({
      selector: 'app-order-details',
      imports: [OrderCard, Spinner],
      template: `
        @if (order.isLoading()) { <app-spinner /> }
        @if (order.error()) {
          <p role="alert">Could not load the order.</p>
          <button (click)="order.reload()">Retry</button>
        }
        @if (order.hasValue()) { <app-order-card [order]="order.value()" /> }
      `,
    })
    export class OrderDetails {
      orderId = input<string>();
    
      order = httpResource<Order>(() => {
        const id = this.orderId();
        return id ? `/api/orders/${id}` : undefined;
      });
    }
    What interviewers listen for
    • Request function reads orderId() and returns a URL
    • undefined means no selection, so nothing loads
    • A new id aborts the old request
    • Guard with hasValue(); retry with reload()
    • Test rapid changes with HttpTestingController

    Likely follow-up: How would you keep the previous order visible, dimmed, while the next one loads?

  13. 13.How would you check whether a feature is ready for zoneless change detection?hard

    Without zone.js, Angular renders only when something notifies it: a signal read by a template changes, a template or host listener runs, markForCheck() is called (the async pipe does this), or ComponentRef.setInput() sets an input. So I look for state that changes without any of those. The classic case is a plain field assigned inside setTimeout, setInterval, a subscribe() callback, a .then(), or a third-party callback such as a map's click event. With zone.js those updated the screen. Zoneless, they silently don't.

    Next, code waiting on NgZone.onStable or onMicrotaskEmpty, which never emit without zones; afterNextRender replaces it. With SSR, non-HTTP async work before rendering needs PendingTasks so the server waits for it.

    The fastest audit is the test suite. Run it with provideZonelessChangeDetection() (provideExperimentalZonelessChangeDetection() in v19), prefer await fixture.whenStable(), and fix every NG0100 that TestBed throws for state changed without a notification. Components already on OnPush with signals usually pass unchanged.

    What interviewers listen for
    • Zoneless renders only on signals, template events, markForCheck() or setInput()
    • Fields set in timers, subscriptions or library callbacks stop updating
    • NgZone.onStable never emits; use afterNextRender
    • PendingTasks for non-HTTP async work under SSR
    • Run tests zoneless and fix each NG0100

    Likely follow-up: Why are OnPush components that use signals usually zoneless-ready already?

  14. 14.How would you diagnose excessive change detection work on a slow Angular screen?mid

    I'd record the slow interaction with the Angular DevTools profiler. It shows each change detection cycle, what triggered it and how long each component took. Two patterns come up most.

    Too many cycles: a setInterval, a mousemove or scroll listener, or a third-party library running timers inside the zone, each triggering a check of the whole tree dozens of times a second. The fix is running that code outside Angular with NgZone.runOutsideAngular(), or going zoneless, and notifying Angular only when something visible changes.

    Expensive cycles: the default strategy checking a large tree, or templates calling methods like filteredRows() that sort thousands of items on every pass. Here I'd move work into computed() signals or pure pipes, add track row.id to big loops, and switch components to OnPush, after checking they don't mutate objects in place, because OnPush won't notice those changes.

    Then I'd record the same interaction again and compare, rather than assuming the change helped.

    What interviewers listen for
    • Profile the real interaction in Angular DevTools
    • Too many cycles: timers or listeners inside the zone
    • Expensive cycles: template methods and large default-strategy trees
    • computed(), pure pipes, track by id, then OnPush
    • Measure the same interaction again

    Likely follow-up: What exactly marks an OnPush component for checking?

  15. 15.When is an Observable a better fit than a Promise in Angular?mid

    A Promise is one value, delivered once, and its work starts as soon as you create it. An Observable can deliver many values over time, does nothing until you subscribe, and can be torn down by unsubscribing.

    So Observables fit anything ongoing: form valueChanges, router events, WebSocket messages, or a search box where debounceTime, distinctUntilChanged and switchMap cancel the previous request. That cancellation is real with HttpClient: unsubscribing aborts the underlying request, while a fetch() Promise needs an AbortController for the same thing. It only works when the source implements teardown, though; wrapping a Promise in from() doesn't make its work cancellable.

    One gotcha: HttpClient Observables are cold, so two subscriptions send two requests.

    For a single result and simple flow, a Promise with async/await is fine, and firstValueFrom() converts when you need one. In Angular 19 and 20 much one-off loading also moves to signals through resource() or httpResource.

    What interviewers listen for
    • Promise: one eager value; Observable: many lazy values
    • Operators like switchMap and debounceTime for streams
    • Unsubscribing aborts an HttpClient request
    • Cold Observables run once per subscription
    • firstValueFrom() for single results

    Likely follow-up: Two components subscribe to the same http.get() Observable. What happens, and how would you share the result?

  16. 16.How would you expose shared reactive state from an Angular service safely?hard

    Keep the writable part private and expose read-only state plus named operations. In the signal version the store holds a private signal<CartItem[]>, and publishes items.asReadonly(), a computed() total, and methods like add() and remove(). Components can read and call methods, but can't set arbitrary values.

    With RxJS it's the same idea: a private BehaviorSubject and a public state$ = this.subject.asObservable(). Exposing the Subject itself lets any consumer call next() with junk, or error() and complete(), which end the stream for every subscriber, permanently. The same failure happens by accident when an HTTP error flows into a shared pipe without a catchError inside the inner switchMap.

    Updates should be immutable, update(list => [...list, item]), so computed() and OnPush see a new reference.

    Finally, lifetime: providedIn: 'root' means one instance for the app, but if someone also adds the service to a component's providers, that component gets its own copy. I'd test two consumers sharing an update.

    @Injectable({ providedIn: 'root' })
    export class CartStore {
      private readonly items = signal<CartItem[]>([]);
    
      readonly lines = this.items.asReadonly();
      readonly total = computed(() =>
        this.items().reduce((sum, i) => sum + i.price * i.qty, 0),
      );
    
      add(item: CartItem) {
        this.items.update((list) => [...list, item]);
      }
    
      remove(id: string) {
        this.items.update((list) => list.filter((i) => i.id !== id));
      }
    }
    What interviewers listen for
    • Private writable signal or Subject
    • Expose asReadonly() or asObservable() plus methods
    • error() or complete() on a shared Subject ends it for everyone
    • catchError inside the inner Observable
    • Immutable updates; check where it's provided

    Likely follow-up: When would you move from a hand-written service like this to NgRx SignalStore?

  17. 17.How would you build a search-as-you-type stream with RxJS?mid

    Start from the control's valueChanges, normalize with map(q => q.trim()), then debounceTime(300) and distinctUntilChanged() so pauses, not keystrokes, trigger requests and repeats are skipped.

    switchMap does the important part: a new query unsubscribes from the previous request, which aborts it, so a slow old response can't overwrite newer results. Short queries still pass through it and map to an idle state instead of being dropped with filter; otherwise clearing the box leaves stale results while the last request lands.

    Errors are caught inside the inner pipe. A catchError on the outer stream completes it, and search silently stops after the first failure.

    Loading state comes from the same stream via startWith, so each request carries its own status. A shared loading flag set in a tap before switchMap and cleared in finalize breaks, because a cancelled request's finalize runs after the new query's tap and hides the spinner while the new request is still running. I'd test with fake timers and out-of-order responses.

    readonly query = new FormControl('', { nonNullable: true });
    
    readonly results = toSignal(
      this.query.valueChanges.pipe(
        map((q) => q.trim()),
        debounceTime(300),
        distinctUntilChanged(),
        switchMap((q) =>
          q.length < 2
            ? of({ status: 'idle' as const })
            : this.api.search(q).pipe(
                map((items) => ({ status: 'done' as const, items })),
                catchError(() => of({ status: 'error' as const })),
                startWith({ status: 'loading' as const }),
              ),
        ),
      ),
      { initialValue: { status: 'idle' as const } },
    );
    What interviewers listen for
    • trim, debounceTime, distinctUntilChanged
    • switchMap cancels the previous request
    • Map short queries to idle instead of filtering them
    • catchError inside the inner pipe
    • Per-request loading state with startWith

    Likely follow-up: How would you test the debounce and out-of-order responses deterministically?

  18. 18.How do you choose between concatMap, mergeMap, switchMap and exhaustMap for saving edits?hard

    I start from what the user expects to happen to each save.

    • concatMap queues saves and runs them in order. It's my default for edits to one record, such as autosaving field changes, because the server sees them in the order they were made.
    • exhaustMap ignores new triggers while a save is running. It fits a Submit button: a double click sends one request.
    • mergeMap runs saves in parallel. It suits independent writes, like saving several unrelated rows, but responses can arrive in any order.
    • switchMap cancels the previous save when a new one starts. Great for reads, risky for writes: unsubscribing aborts the request in the browser, but the server may already have committed it, leaving a write you never confirmed.

    None of these undo anything on the server. If order really matters, send a version number so the server rejects stale writes with a 409, and an idempotency key so a retried request isn't applied twice.

    What interviewers listen for
    • concatMap keeps saves in order
    • exhaustMap ignores clicks while saving
    • mergeMap runs in parallel with no ordering
    • switchMap cancels in the browser, not on the server
    • Version numbers and idempotency keys on the server

    Likely follow-up: How would you autosave so only the latest state is sent, without reordering writes?

  19. 19.A component like this one keeps reacting to events after the user has left its route. How would you find and fix the leak?mid

    Two things here outlive the component: the updates$ subscription and the resize listener. Leaving the route destroys the component, but the root PriceFeed still holds a subscriber whose callback closes over it, so the callback keeps running and the component can't be garbage collected.

    I'd list everything the component starts: subscriptions to long-lived streams, window or document listeners, setInterval, ResizeObserver, WebSocket handlers and third-party callbacks. HTTP calls complete on their own; most of the rest don't.

    Then tie each one to the component's lifetime. takeUntilDestroyed() ends a subscription when called in the constructor or a field initializer, or when given a DestroyRef. toSignal() and the async pipe subscribe and unsubscribe for you. DestroyRef.onDestroy() removes manual listeners, and a host listener like (window:resize) is cleaned up automatically.

    To confirm the fix, I'd take heap snapshots in Chrome DevTools after leaving the route a few times and search for LiveTicker. Retained instances show what's holding them.

    export class LiveTicker implements OnInit {
      private prices = inject(PriceFeed);
      latest = signal<Price | null>(null);
    
      ngOnInit() {
        this.prices.updates$.subscribe((p) => this.latest.set(p));
        window.addEventListener('resize', () => this.layout());
      }
    
      layout() {
        // recompute column widths
      }
    }
    What interviewers listen for
    • List subscriptions, listeners, timers and library callbacks
    • Root services keep component callbacks alive
    • takeUntilDestroyed(), toSignal() or the async pipe
    • DestroyRef.onDestroy() for manual listeners
    • Heap snapshots show retained instances

    Likely follow-up: Why does takeUntilDestroyed() throw when it's called inside ngOnInit without arguments?

  20. 20.How do you decide where to convert an Observable into a signal?hard

    I keep RxJS where time and cancellation matter, and convert once, where the component consumes the result. A search stream keeps debounceTime and switchMap in RxJS, because signals have no notion of time or of cancelling a request. Its final value becomes results = toSignal(results$, { initialValue: [] }), and the template reads results() without an async pipe.

    Two decisions come with the conversion. The initial value: without initialValue the type includes undefined, and requireSync: true works only for sources that emit synchronously, like a BehaviorSubject. And errors: if the Observable errors, reading the signal throws, so I catch errors upstream and turn them into a state value.

    Mistakes I look for: calling toSignal() inside a method or a computed(), which throws outside an injection context or, given an injector, creates a new subscription on every call; and bouncing between toSignal() and toObservable(), which adds asynchronous hops and makes data flow hard to follow. One conversion per stream, in a field initializer, is the pattern.

    What interviewers listen for
    • Keep debounceTime and switchMap in RxJS
    • Convert once with toSignal() where the component reads it
    • Choose initialValue or requireSync
    • An Observable error makes the signal throw on read
    • No toSignal() in methods or computed()

    Likely follow-up: When would you go the other way with toObservable()?

  21. 21.How would you validate a username against the server without flooding it with requests?hard

    Three things keep the request count down. First, sync validators go first: Angular runs async validators only when every sync validator passes, so required and a pattern check stop empty or malformed names from reaching the server.

    Second, debounce inside the validator. On each value change Angular unsubscribes from the previous async validation, so starting with timer(400) before switchMap to the API means only a pause in typing sends a request, and stale checks are cancelled. updateOn: 'blur' is the simpler option if checking on blur is acceptable.

    Third, the returned Observable must complete. HTTP does; a long-lived stream would leave the control PENDING forever.

    While the check runs, the status is PENDING, so I show 'Checking' and disable Submit while form.pending is true. A failed check returns a distinct error such as checkFailed, not taken. And the server still enforces uniqueness on submit, because two people can claim the same name between check and save.

    function usernameAvailable(api: UserApi): AsyncValidatorFn {
      return (control) =>
        timer(400).pipe(
          switchMap(() => api.isAvailable(control.value)),
          map((available) => (available ? null : { taken: true })),
          catchError(() => of({ checkFailed: true })),
        );
    }
    
    username = new FormControl('', {
      nonNullable: true,
      validators: [Validators.required, Validators.pattern(/^[a-z0-9_]{3,20}$/)],
      asyncValidators: [usernameAvailable(inject(UserApi))],
    });
    What interviewers listen for
    • Async validators run only after sync ones pass
    • timer() then switchMap debounces inside the validator
    • The Observable must complete or the status stays PENDING
    • Show pending; distinguish taken from a failed check
    • The server still enforces uniqueness

    Likely follow-up: Why doesn't adding debounceTime(400) to the validator's pipe debounce anything?

  22. 22.How would you show server-side validation errors on an Angular reactive form?mid

    Say the API answers with a 422 and a body like { errors: [{ field: 'address.zip', message: 'Unknown postcode' }] }. For each error I look up the control with form.get(field), which accepts dot paths into nested groups, and call setErrors({ server: message }) on it. The template then shows it like any other validation error. Errors with no matching control, such as 'This order has already shipped', go into a form-level message instead of being dropped.

    I don't reset the form; the user's values stay as they were. Clearing is mostly automatic, because errors set with setErrors are replaced the next time that control's validators run, which happens as soon as the user edits the field.

    For accessibility, markAllAsTouched() makes errors render, each input gets aria-invalid and an aria-describedby pointing at its message, and focus moves to an error summary at the top or to the first invalid field. A toast alone is easy to miss. Other failures get a generic 'Couldn't save, try again' with the form intact.

    What interviewers listen for
    • form.get(field)?.setErrors({ server: message })
    • Unmatched errors go to a form-level message
    • Keep the user's values; edits clear server errors
    • markAllAsTouched(), aria-describedby and focus the summary

    Likely follow-up: How would you show a server error that applies to two fields at once, such as an invalid date range?

  23. 23.How would you make a custom star-rating component work with Angular reactive forms?mid

    I'd implement ControlValueAccessor and register the component with NG_VALUE_ACCESSOR and multi: true. The four methods each cover one direction:

    • writeValue(v): the form sets the value, on init, setValue() or reset(). Update the display only.
    • registerOnChange(fn): store fn and call it when the user picks a star.
    • registerOnTouched(fn): call it on blur, so touched and updateOn: 'blur' work.
    • setDisabledState(isDisabled): make the stars inert when the control is disabled.

    The classic bug is calling onChange inside writeValue. A programmatic reset() then marks the form dirty and fires valueChanges as if the user had clicked.

    For accessibility, the simplest approach is five native radio inputs styled as stars, inside a group with a label. That gives arrow-key support and correct screen reader announcements without custom ARIA. I'd test that reset() leaves the form pristine, disable() blocks input, blur marks it touched, and the keyboard can change the value, not only the mouse.

    @Component({
      selector: 'app-star-rating',
      templateUrl: './star-rating.html', // five native radio inputs
      providers: [
        { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StarRating), multi: true },
      ],
    })
    export class StarRating implements ControlValueAccessor {
      protected value = signal<number | null>(null);
      protected disabled = signal(false);
      private onChange: (v: number) => void = () => {};
      protected onTouched: () => void = () => {};
    
      writeValue(v: number | null) { this.value.set(v); }
      registerOnChange(fn: (v: number) => void) { this.onChange = fn; }
      registerOnTouched(fn: () => void) { this.onTouched = fn; }
      setDisabledState(isDisabled: boolean) { this.disabled.set(isDisabled); }
    
      protected select(rating: number) {
        this.value.set(rating);
        this.onChange(rating);
      }
    }
    What interviewers listen for
    • Implement ControlValueAccessor with NG_VALUE_ACCESSOR
    • writeValue updates the display and never calls onChange
    • Call onTouched on blur; handle setDisabledState
    • Native radio inputs for keyboard and screen readers
    • Test reset(), disable(), blur and keyboard input

    Likely follow-up: If the control starts at 0 instead of null, why does Validators.required pass, and what would you use instead?

  24. 24.How would you evaluate adopting Signal Forms in an existing reactive-forms application?hard

    Version comes first. Signal Forms don't exist in Angular 19 or 20; they arrived in v21 as an experimental API in @angular/forms/signals. On 19 or 20 the real first step is the upgrade, and I'd check the API's status in the target version.

    The appeal is real. form(model) wraps a signal you already have instead of copying the value, validation and conditional rules live in one schema function, and a cross-field rule reruns when either field changes, with no valueChanges subscriptions.

    Then the cost. I'd prototype one representative form, not a toy: async validation, server errors mapped onto fields, a custom control and a reset. I'd also list every custom ControlValueAccessor and third-party control we use, since each needs to work with the new field binding.

    Reactive forms remain fully supported, so there's no deadline. For an app with hundreds of working reactive forms, I'd build new forms with Signal Forms and migrate old ones only when they're being rewritten anyway, keeping behavior tests for validation, reset and submit.

    What interviewers listen for
    • Signal Forms need Angular 21 or later
    • form(model) wraps your signal instead of copying it
    • Prototype a real form with async and server errors
    • Check custom ControlValueAccessor and third-party controls
    • New forms first; migrate old ones when rewritten

    Likely follow-up: What problem with reactive forms does wrapping the model signal solve?

  25. 25.How would you protect an Angular route without mistaking route guards for security?mid

    A guard is a UX feature. It decides what the browser shows, but it runs on the user's machine, so anyone can bypass it by editing the JavaScript or calling the API directly. Real protection lives on the server: every endpoint checks the session and the permission and answers 401 or 403.

    On the client, a functional CanActivateFn sends signed-out users to /login by returning router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } }). Returning a UrlTree lets the router treat it as a clean redirect. For lazy features I use canMatch: when it fails, the router doesn't even download the chunk.

    After login, I follow returnUrl only if it's an in-app path starting with a single /. router.navigateByUrl() stays inside the app, but if that value ever reaches window.location or a server-side login page, an unchecked //evil.com becomes an open redirect.

    Sessions also expire mid-use, so an interceptor handles 401 by clearing state and redirecting to login, and 403 with a no-access page. Tokens never go in query parameters.

    export const authGuard: CanActivateFn = (_route, state) => {
      const auth = inject(AuthService);
      const router = inject(Router);
      return auth.isSignedIn()
        ? true
        : router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } });
    };
    
    export const routes: Routes = [
      { path: 'account', component: AccountPage, canActivate: [authGuard] },
      { path: 'billing', canMatch: [financeOnly], loadChildren: () => import('./billing/routes') },
    ];
    What interviewers listen for
    • Guards are UX; the server checks every API call
    • Return a UrlTree with a returnUrl
    • canMatch stops the lazy chunk from loading
    • Follow only in-app returnUrl paths
    • Handle 401 and 403 after the page loads

    Likely follow-up: What's the difference between canActivate and canMatch on a lazy-loaded route?

  26. 26.How would you structure routes for a feature with a shared layout and several child screens?mid

    A parent route renders the shared layout and child routes fill its <router-outlet />. Here OrdersLayout holds the tabs and outlet; the screens are children. The whole feature sits behind loadChildren, so none of it downloads until someone opens /orders.

    Two lifetime decisions matter. Services in the route's providers are shared by the layout and every child, but that injector is created on the first visit and, by default, lives as long as the app. That's fine for a cache of orders and wrong for per-visit state like a wizard draft, which belongs in the layout component's own providers so it's destroyed on exit.

    The second is component reuse. Going from /orders/1 to /orders/2 keeps the same OrderDetail instance, so ngOnInit doesn't run again. Read the id reactively, through an id = input() with withComponentInputBinding() or the paramMap Observable, not from snapshot once.

    I'd test deep links on a fresh load, refresh, back and forward, and switching between ids.

    // app.routes.ts
    { path: 'orders', loadChildren: () => import('./orders/orders.routes') },
    
    // orders/orders.routes.ts
    export default [
      {
        path: '',
        component: OrdersLayout, // tabs plus <router-outlet />
        providers: [OrdersApi],
        children: [
          { path: '', component: OrderList },
          { path: ':id', component: OrderDetail },
          { path: ':id/edit', component: OrderEdit, canDeactivate: [unsavedChangesGuard] },
        ],
      },
    ] satisfies Routes;
    What interviewers listen for
    • Parent layout with <router-outlet /> and child routes
    • loadChildren for the whole feature
    • Route providers live on after you leave
    • :id changes reuse the same component
    • Test deep links, refresh, back and forward

    Likely follow-up: How would you keep the list's filters and scroll position when the user returns from a detail page?

  27. 27.Several requests fail with 401 at the same moment. How would you stop each one from triggering its own token refresh?hard

    Share one refresh. The auth service keeps the in-flight refresh as a single Observable, the refresh POST piped through shareReplay(1), created by the first 401 and cleared when it settles. Every request that gets a 401 waits on that same Observable, then retries once with the new token. Ten parallel failures produce one refresh call.

    Three guards stop loops. The refresh request itself carries an HttpContextToken flag such as SKIP_AUTH, so the interceptor never tries to refresh the refresh. Each original request retries only once, tracked with another context flag, so a 401 after a successful refresh goes to login instead of looping. And if the refresh fails, the service clears the tokens, fails every waiting request and sends the user to sign in.

    The subtle case is identity. If someone else signs in while old requests are still waiting, those requests must not be replayed with the new person's token. Tagging requests with a session id and dropping retries from an older session prevents that.

    What interviewers listen for
    • One shared refresh Observable with shareReplay(1)
    • Waiting requests retry once with the new token
    • Skip the refresh call with an HttpContextToken
    • Failed refresh clears tokens and goes to login
    • Never replay requests under a different user

    Likely follow-up: Why can refresh-token rotation still break when two browser tabs refresh at the same time?

  28. 28.Why is a single boolean loading flag, like the one in this interceptor, wrong for overlapping requests?mid

    This interceptor has two bugs. With overlapping requests, the first one to finish sets active to false while the second is still running, so the spinner vanishes early. And tap({ complete }) covers only success. An error, or a request cancelled by switchMap or a destroyed component, never completes, so the spinner can stick forever.

    I'd count instead: pending = signal(0) and isLoading = computed(() => this.pending() > 0). The interceptor increments before next(req) and decrements in finalize(), which runs on completion, error and unsubscribe alike.

    Not every request should show it. Background polling or a silent prefetch sets a context flag, new HttpContext().set(SILENT, true), and the interceptor skips those. A global overlay should also wait 150 to 200 milliseconds before appearing, so fast requests don't flash it.

    Finally, a global indicator only fits blocking actions. Refreshing one widget deserves a loading state on that widget, often straight from its resource or stream, not a full-page spinner.

    export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
      const loading = inject(LoadingService);
      loading.active.set(true);
      return next(req).pipe(
        tap({ complete: () => loading.active.set(false) }),
      );
    };
    What interviewers listen for
    • The first response clears the flag while others run
    • Count pending requests in a signal
    • Decrement in finalize() for complete, error and cancel
    • Skip background requests with an HttpContextToken
    • Local loading state for non-blocking work

    Likely follow-up: Where would you add a delay so fast requests don't flash the spinner?

  29. 29.How would you test a component's HTTP failure and retry flow?mid

    I'd drive it the way a user would and control the network at the HTTP layer. TestBed gets provideHttpClient() and provideHttpClientTesting(), and the test injects HttpTestingController, so the real service and the real subscription code stay in play.

    The test types a comment and clicks Post. http.expectOne('/api/comments') returns the pending request, and flush('Oops', { status: 500, statusText: 'Server Error' }) fails it. For a network failure, req.error(new ProgressEvent('error')) is the closer simulation. After await fixture.whenStable(), I assert what the user sees: the error message, and the textarea still holding their text.

    Then the retry: click the retry button, expect a second request, flush a success, and assert the success state. http.verify() at the end fails the test if a request was left unhandled or an extra one was sent.

    What I avoid is setting a private error field and checking the template, because that skips the code most likely to be broken: the subscription, the error mapping and the retry wiring.

    it('keeps the comment and retries after a server error', async () => {
      TestBed.configureTestingModule({
        imports: [CommentBox],
        providers: [provideHttpClient(), provideHttpClientTesting()],
      });
      const fixture = TestBed.createComponent(CommentBox);
      const http = TestBed.inject(HttpTestingController);
      const el: HTMLElement = fixture.nativeElement;
      await fixture.whenStable();
    
      const box = el.querySelector('textarea')!;
      box.value = 'Great post';
      box.dispatchEvent(new Event('input'));
      el.querySelector<HTMLButtonElement>('[data-test=post]')!.click();
    
      http.expectOne('/api/comments').flush('Oops', { status: 500, statusText: 'Server Error' });
      await fixture.whenStable();
      expect(el.textContent).toContain('Could not post your comment');
      expect(box.value).toBe('Great post');
    
      el.querySelector<HTMLButtonElement>('[data-test=retry]')!.click();
      http.expectOne('/api/comments').flush({ id: 7, text: 'Great post' });
      await fixture.whenStable();
      expect(el.textContent).toContain('Comment posted');
      http.verify();
    });
    What interviewers listen for
    • provideHttpClientTesting() with HttpTestingController
    • Trigger the real user action
    • flush() a 500, or error() for network failures
    • Assert the message and the preserved input
    • Retry through the UI, then verify()

    Likely follow-up: How would you test a retry policy that waits between attempts without slowing the test down?

  30. 30.How would you test a parameterized Angular detail route such as /orders/:id?mid

    I'd use the real route configuration with RouterTestingHarness instead of mocking ActivatedRoute. TestBed provides provideRouter(routes, withComponentInputBinding()) plus the HTTP testing providers. Then const harness = await RouterTestingHarness.create() and await harness.navigateByUrl('/orders/1', OrderDetail) render the component the way the router would, guards and resolvers included.

    The test that catches real bugs moves to a second id on the same route with harness.navigateByUrl('/orders/2'). The router reuses the same OrderDetail instance, so code that read snapshot.paramMap once in ngOnInit keeps showing order 1. I'd assert that the second request goes to /api/orders/2 and the heading updates.

    Then the edge cases: an unknown id that returns 404 should show a not-found state or redirect, and a guarded URL for a signed-out user should end on the login page, which TestBed.inject(Router).url confirms.

    A mocked ActivatedRoute with a fixed of({ id: '1' }) can't catch any of this, because the parameter never changes.

    What interviewers listen for
    • RouterTestingHarness with the real routes
    • Move from /orders/1 to /orders/2
    • The same component instance is reused
    • Cover not-found and guard redirects
    • A fixed mocked ActivatedRoute hides these bugs

    Likely follow-up: How would you test a canDeactivate guard that warns about unsaved changes?

  31. 31.How would you review an Angular SSR page for accidental exposure of private data?hard

    I'd review everything the server sends, not just what's visible. Fetch the raw HTML with curl or the Network tab and look for three things.

    • Transfer state. With provideClientHydration(), the HTTP transfer cache serializes server-side GET responses into a JSON <script> in the page. Requests with an Authorization header are skipped by default, but cookie-authenticated ones aren't, so a full user record can end up in the HTML even when the template shows only a name.
    • Shared server state. Each request gets a fresh application and root injector, but module-level variables in the server bundle are shared by every request in that Node process. A let currentUser at module scope leaks one user's data into another user's page.
    • Caching. Personalized HTML needs Cache-Control: private or no-store, or a CDN may serve it to the next visitor. A route set to RenderMode.Prerender bakes its data into a static file at build time.

    Then I'd render the same URL for two users concurrently and compare the responses.

    What interviewers listen for
    • Inspect the raw HTML, including transfer state
    • Cookie-authenticated GET responses get serialized
    • Module-level variables are shared across requests
    • Cache-Control: private for personalized pages
    • Render two users concurrently and compare

    Likely follow-up: How would you keep one sensitive request out of the HTTP transfer cache?

  32. 32.How would you debug an Angular hydration error around a third-party widget?hard

    NG0500 errors say what Angular expected at a position and what it found, such as expected <div> but found <canvas>. Angular's hydration compares DOM structure, meaning nodes and tag names, not text, so the cause is something adding, removing or moving elements.

    With a third-party widget, that's usually one of two things. Either the library ran during server rendering and produced markup the client doesn't recreate, or it ran in the browser before hydration finished and rewrote its container, so Angular finds nodes it didn't create.

    The fix is starting the widget in afterNextRender(), which runs only in the browser and after hydration, in a component whose template is an empty container. If the library can't be tamed yet, ngSkipHydration on that component's host element makes Angular discard its server DOM and render it fresh on the client. It only works on component hosts, so treat it as a quarantine.

    I'd also rule out invalid HTML, like a <div> inside a <p>, which the browser silently restructures.

    What interviewers listen for
    • Read what Angular expected versus what it found
    • Hydration compares structure, not text
    • Start the widget in afterNextRender()
    • ngSkipHydration on the host is a temporary quarantine
    • Rule out invalid HTML nesting

    Likely follow-up: Why does afterNextRender() avoid the mismatch when ngOnInit doesn't?

  33. 33.Which parts of a page would you put in a @defer block, and how would you check that it helped?mid

    Heavy content that isn't needed for the first view: a chart dashboard below the fold, a rich-text editor behind an Edit button, a comments section, a map. Writing @defer (on viewport) { <sales-chart /> } @placeholder { <div class="chart-skeleton"></div> } loads the chart's code only when the placeholder scrolls into view. For the editor, on interaction fits better, with prefetch on idle so the click doesn't wait on the network.

    I wouldn't defer the main content, the LCP element or anything needed for the first interaction, and the placeholder should reserve the final height so the layout doesn't jump.

    Then I'd verify, because a defer block can silently save nothing. A dependency is split out only if it's standalone and nothing else in the same file references it; a @ViewChild on it, or one more use outside the block, keeps it in the eager bundle. The build output should list a new lazy chunk. And with SSR, the server renders only the placeholder, unless incremental hydration with hydrate on triggers is in use.

    What interviewers listen for
    • Defer heavy, below-the-fold or on-demand content
    • on viewport, on interaction, prefetch on idle
    • Never the main content or the LCP element
    • The placeholder reserves space
    • Check the build output for a lazy chunk

    Likely follow-up: What do the parameters in @loading (after 100ms; minimum 1s) prevent?

  34. 34.How would you speed up a large Angular data table?hard

    First I'd measure which part is slow, because the fixes differ. The Angular DevTools profiler and the Chrome Performance panel separate network time, change detection and DOM work.

    If rendering is the cost:

    • @for (row of rows(); track row.id) lets a refresh update existing rows instead of recreating them.
    • Method calls in cells, such as {{ formatDate(row.date) }}, run for every row on every check. A pure pipe or precomputed values fix that.
    • Filtering and sorting become computed() signals, with the filter input debounced.
    • OnPush row components mean editing one row doesn't check thousands.

    If there are simply too many rows, page and sort on the server, or render only the visible window with the CDK's cdk-virtual-scroll-viewport. Virtual scrolling has costs: browser find can't see rows that aren't rendered, focus can be lost when a row scrolls out, and screen readers need aria-rowcount and aria-rowindex.

    I'd test with production-sized data, say 10,000 rows with live updates, not a 20-row fixture.

    What interviewers listen for
    • Measure network, change detection and DOM separately
    • track row.id and no method calls in cells
    • computed() filtering and OnPush rows
    • Server paging or cdk-virtual-scroll-viewport
    • Virtual scrolling hurts find-in-page and focus

    Likely follow-up: When would you choose server-side paging over virtual scrolling?

  35. 35.How would you render user-authored rich text, stored as HTML, safely in Angular?hard

    Angular already sanitizes values bound with [innerHTML]: it removes <script> elements and on* attributes such as onerror, neutralizes javascript: URLs, and logs a warning in development when it strips something. So the first rule is not to undo that. Calling DomSanitizer.bypassSecurityTrustHtml() on user content turns a stored comment into stored XSS.

    Angular's sanitizer is a safety net, not a content policy. I'd define what the product allows, say paragraphs, lists, links, bold and images from our CDN, and enforce it on the server when content is saved, with an allowlist sanitizer such as DOMPurify. That also protects emails or a mobile app that render the same HTML without Angular.

    User content is never compiled as an Angular template; that would be code execution, not markup. Links get rel="noopener noreferrer" and only http, https or mailto schemes.

    A Content Security Policy with Trusted Types adds defense in depth. I'd test with stored payloads like <img src=x onerror=alert(1)>, including rows saved before the policy existed.

    What interviewers listen for
    • [innerHTML] is sanitized automatically
    • Never bypassSecurityTrustHtml() on user content
    • Allowlist sanitizer on the server at save time
    • Never compile user content as a template
    • CSP with Trusted Types; test stored payloads

    Likely follow-up: Why sanitize on the server when Angular already sanitizes in the browser?

  36. 36.How would you stop a tenant switch from showing the previous tenant's data?hard

    I'd treat the tenant as part of every key. It goes in the URL, /t/:tenantId/..., and an interceptor adds it to API calls from one currentTenant signal.

    On a switch, three things go wrong if you're not careful:

    • Cached state. Stores filled for tenant A still hold A's data. Key caches by tenant id, or reset every tenant-scoped store when the id changes. Neither route providers nor components reused from /t/a to /t/b reset on their own.
    • Late responses. A slow request for A can land after the switch. Loads driven by the tenant signal through switchMap or a resource cancel old requests, and any response whose tenant doesn't match the current one is dropped.
    • The visible screen. Clear A's data immediately instead of leaving its dashboard up while B loads.

    A full page load on switch is a legitimate, simple option in a complex app. And the server checks membership on every request, never trusting the tenant id the client sends.

    What interviewers listen for
    • Tenant id in the URL and in every API call
    • Key or reset caches and stores per tenant
    • Cancel or drop responses for the old tenant
    • Clear the screen right away
    • The server checks membership on every request

    Likely follow-up: Besides your own stores, what in the browser can still hold the old tenant's data?

  37. 37.Does using Angular Material mean a page is accessible?mid

    No. Material gives you good building blocks: MatDialog traps focus and returns it to the trigger on close, mat-form-field links mat-error messages to the input, and menus and selects support the keyboard. Putting them together correctly is still your job.

    On a Material page I'd review:

    • Every field has a mat-label, and icon-only buttons such as mat-icon-button have an aria-label.
    • Custom theme colors still meet contrast, 4.5:1 for body text, and nobody removed focus outlines in global CSS.
    • Async results like 'Saved' or '12 results' are announced, for example with the CDK's LiveAnnouncer.
    • Each route sets a page title, headings are in order, and focus lands somewhere sensible after a route change.
    • Tables with matSort describe the sort state, and the paginator works from the keyboard.

    Then testing. axe in CI catches some issues, but I'd also walk the main flows with only a keyboard and with a screen reader such as NVDA or VoiceOver, including errors and dialogs.

    What interviewers listen for
    • Material handles focus traps, ARIA and keyboard basics
    • Labels on fields and icon-only buttons
    • Theme contrast and visible focus
    • LiveAnnouncer for async updates; page titles
    • axe plus keyboard and screen reader testing

    Likely follow-up: What does cdkFocusInitial change when a dialog opens?

  38. 38.What should a wrapper around the Angular CDK Overlay clean up?mid

    The key fact is that overlay content doesn't live inside your component. overlay.create() appends a pane to .cdk-overlay-container under body, so destroying the component that opened it doesn't dispose the overlay: the pane, backdrop and keyboard listeners stay behind. I call overlayRef.dispose() on close and again in DestroyRef.onDestroy. dispose() detaches the portal, destroys the attached view, removes the pane and backdrop, and completes streams like backdropClick(), keydownEvents() and detachments(). I use detach() only when the same overlay will be reattached.

    Subscriptions I added to those streams, plus any manual document listeners, end at the same point. Scroll behavior comes from a scroll strategy such as reposition(), close() or block(), not hand-written scroll listeners.

    Focus is the other half. Save document.activeElement before opening, trap focus inside a modal panel with cdkTrapFocus, and restore focus to the trigger on close.

    Then test the awkward cases: open and close it twenty times and check there's only one pane, change routes while it's open, and close it with Escape.

    What interviewers listen for
    • Overlay panes live under body, outside your component
    • dispose() on close and in DestroyRef.onDestroy
    • End subscriptions to backdropClick() and keydownEvents()
    • Trap focus and restore it to the trigger
    • Test repeated opening and route changes

    Likely follow-up: What's the difference between detach() and dispose() on an OverlayRef?

  39. 39.What checks would you put in CI for an Angular application?mid

    I'd order them from fast to slow so failures show up early:

    • Install from the lockfile with npm ci, so CI builds exactly what was tested locally.
    • Lint with angular-eslint, including its template rules.
    • Templates. ng build with strictTemplates: true catches template type errors, wrong input names and missing imports. Plain tsc doesn't check templates at all.
    • Unit and component tests with ng test in headless mode, whether the runner is Karma, Jest or Vitest. Component tests should drive the DOM, not set private fields.
    • Production build with budgets in angular.json, for example failing if the initial bundle passes 1 MB. A budget failure catches the dependency someone imported by accident.
    • End-to-end tests with Playwright or Cypress on a few critical flows, such as sign-in, checkout and the main form, plus an axe scan of key pages.

    I'd also read build warnings, such as CommonJS dependency warnings, and fail on new ones. In a large repo, running only affected projects, with nx affected or similar, keeps CI fast.

    What interviewers listen for
    • npm ci, lint and strictTemplates
    • Component tests that drive the DOM
    • Production build with bundle budgets
    • Playwright or Cypress on critical flows
    • New build warnings fail the run

    Likely follow-up: What kinds of bugs does strictTemplates catch that unit tests often miss?

  40. 40.A lazy route downloads far more JavaScript than expected. How would you investigate?hard

    I'd look at the real output first. ng build --stats-json writes a stats file you can load into the esbuild bundle analyzer, or I'd run source-map-explorer on the built files. Either shows which modules ended up in the route's chunk and in the initial bundle. Then the Network tab shows which chunks actually load when I open the route.

    The usual causes:

    • Barrel files. Importing one helper from shared/index.ts can drag in everything it re-exports.
    • Eager references to lazy code. A component lazy-loaded by the router but also imported by an eagerly loaded file lands in the main bundle. Type-only imports are fine, because they're erased.
    • Heavy libraries. lodash instead of lodash-es, moment with every locale, or a chart library imported by a root service.

    The fixes are direct imports instead of barrels, import() inside the function that needs a heavy library, and @defer for heavy widgets. Then rebuild and compare chunk sizes, because adding loadComponent doesn't help while the code is still imported eagerly somewhere else.

    What interviewers listen for
    • ng build --stats-json and a bundle analyzer
    • Check which chunks load in the Network tab
    • Barrel files pull in unrelated code
    • Eager imports of lazy components
    • Heavy libraries like lodash or moment

    Likely follow-up: Why can importing through a barrel file defeat tree shaking?

  41. 41.How would you choose between a service with signals and a state-management library such as NgRx?hard

    I start with the simplest thing that handles the real workflows. For most features, a service with private signals, computed() values and methods is enough: a cart, a filter panel, a settings page. It's plain TypeScript, easy to test, and scoped wherever it's provided.

    I'd consider NgRx when the workflows get hard, not merely when the app gets big. Signs are many features reading and writing the same entities; complex async flows such as optimistic updates with rollback, WebSocket events and undo; a need to see every state change in Redux DevTools; or a large team that benefits from one enforced pattern. NgRx Store gives actions, reducers and effects with that traceability, at the cost of more files and indirection. NgRx SignalStore sits in between: signalStore(withState(...), withComputed(...), withMethods(...)) keeps the signal style with more structure.

    To decide, I'd build one real hard workflow both ways, say approving an invoice while live updates arrive, and compare the code. Adopting a global store just because state exists turns simple code into ceremony.

    What interviewers listen for
    • Signal service by default: private signals, computed(), methods
    • A library when workflows are complex, not just big
    • NgRx Store for actions, effects and DevTools history
    • NgRx SignalStore as a middle ground
    • Prototype one hard workflow both ways

    Likely follow-up: How would you move a feature from a signal service to SignalStore without changing its components?

  42. 42.What would you plan before persisting Angular application state in browser storage?hard

    First, what's safe to keep. UI preferences, filters and unsent drafts are good candidates. Access tokens and personal data aren't: anything in localStorage can be read by any script on the page, including an XSS payload. Loading flags and error messages don't belong there either; a reload shouldn't bring back a spinner.

    Second, a versioned format such as { version: 2, data: {...} }. On load I parse inside try/catch, validate the shape, and either migrate old versions, for example renaming a field from v1 to v2, or discard what I can't read. Bad data in storage shouldn't crash the app at startup.

    Third, scope. Keys include the user id, like prefs:42, and are cleared on sign-out, so a shared computer doesn't show one person's drafts to the next.

    Mechanically, an effect() saves the signal's value, debounced if it changes often, and storage is read only in the browser, since localStorage doesn't exist during SSR. The storage event keeps other open tabs in sync.

    What interviewers listen for
    • Persist preferences and drafts, not tokens or loading flags
    • Versioned format with validation and migration
    • Parse in try/catch and discard bad data
    • Key by user id and clear on sign-out
    • No localStorage during SSR

    Likely follow-up: How would you keep persisted state in sync between two open tabs?

  43. 43.What would you clarify before choosing an Angular translation approach?mid

    The main question is whether users switch language at runtime. Angular's built-in i18n, @angular/localize, compiles one build per locale: you mark text with the i18n attribute or $localize, extract it with ng extract-i18n, and ng build --localize produces /en/, /fr/ and so on. It's fast and catches missing translations at build time, but changing language means loading a different URL. Runtime libraries such as Transloco or ngx-translate load JSON per language and switch instantly, at the cost of runtime lookups and fewer build-time checks.

    Then the rest of localization. Plurals need ICU messages, for example {count, plural, =0 {No items} =1 {One item} other {{{count}} items}}, not string concatenation. Dates, numbers and currency go through pipes driven by LOCALE_ID. Arabic and Hebrew need dir="rtl" and CSS logical properties like margin-inline-start.

    With SSR, the server must pick the locale from the URL or a cookie and render in it, or users see a flash of the wrong language. I'd test with German, for long text, and an RTL locale.

    What interviewers listen for
    • Build-time @angular/localize versus runtime libraries
    • Runtime switching needs Transloco or ngx-translate
    • ICU messages for plurals, not concatenation
    • LOCALE_ID pipes and RTL with logical properties
    • SSR picks the locale from the URL or a cookie

    Likely follow-up: How does one build per locale affect deployment and URLs?

  44. 44.How would you choose the view encapsulation mode for a reusable Angular widget?mid

    The default, ViewEncapsulation.Emulated, suits most widgets. Angular rewrites the component's selectors with generated attributes like _ngcontent-abc, so styles don't leak out, while global styles such as typography and theme CSS still apply inside.

    ShadowDom uses a real shadow root, so isolation works in both directions. Global styles stop applying inside, so fonts, resets or a component library's global CSS can vanish. CSS custom properties still inherit into the shadow tree, so they become the theming API. It's mostly worth it for widgets embedded in pages you don't control, like Angular Elements on third-party sites.

    None makes the styles global, which suits only deliberate global styles.

    Whichever mode, consumers need supported hooks: custom properties like --rating-color, :host classes for variants, and ::part() names with ShadowDom. Otherwise they reach into private markup with the deprecated ::ng-deep and break on your next refactor.

    One gotcha: CDK overlay content moves under body. Emulated content keeps its attributes and stays styled, but content portaled out of a shadow root loses the component's styles.

    What interviewers listen for
    • Emulated by default: scoped selectors, global styles still apply
    • ShadowDom blocks global styles; custom properties still inherit
    • None makes styles global
    • Theme through custom properties and :host classes
    • Avoid ::ng-deep; watch overlay content

    Likely follow-up: How would you let consumers theme a widget that uses ShadowDom?

  45. 45.How would you build an Angular dashboard whose widgets are chosen by configuration from the server?hard

    The configuration should only choose from code we ship. A registry maps each type string to a lazy import, as in the snippet, so every widget gets its own chunk and an unknown type renders a fallback instead of failing. I'd never compile template strings from the server: AOT builds don't include the compiler, and running server-provided markup as a template is code execution.

    Rendering uses supported APIs: NgComponentOutlet with its inputs option in the template, or ViewContainerRef.createComponent() followed by ref.setInput(). Angular 20 also accepts bindings: [inputBinding(...)] in createComponent. A widget that needs its own services gets an injector built with Injector.create, providing its config under an injection token.

    Each widget's settings are validated against a schema before rendering, and the APIs a widget calls check permissions on the server, because the config is just data a client can tamper with. Removing a widget calls ref.destroy(), or clear() on the container, which runs its cleanup. I'd also version the config format so saved layouts survive widget changes.

    const WIDGETS: Record<string, () => Promise<Type<unknown>>> = {
      revenue: () => import('./widgets/revenue').then((m) => m.RevenueWidget),
      tickets: () => import('./widgets/tickets').then((m) => m.TicketsWidget),
    };
    
    // In the dashboard component
    async addWidget(config: WidgetConfig) {
      const load = WIDGETS[config.type];
      if (!load) return this.showUnknown(config.type);
      const ref = this.host.createComponent(await load());
      ref.setInput('settings', config.settings);
      this.refs.push(ref);
    }
    What interviewers listen for
    • A registry maps config types to lazy imports
    • Fallback for unknown types; never compile server templates
    • NgComponentOutlet or createComponent() with setInput()
    • Validate settings; the server checks permissions
    • Destroy removed widgets and version the config

    Likely follow-up: How would you stop one broken widget from taking down the whole dashboard?

  46. 46.Beyond caching assets with a service worker, what does an offline data-entry feature need?hard

    The Angular service worker caches the app shell and, if configured, GET responses, but it doesn't queue writes: a POST sent offline just fails. The feature needs its own outbox:

    • A durable queue. Each change goes into IndexedDB with a client-generated id from crypto.randomUUID(), which the server treats as an idempotency key so a replay doesn't create a duplicate.
    • Honest status. Queued records show as 'Saved on this device, waiting to sync', never as confirmed.
    • Replay. On reconnect, operations go out in order, for example through concatMap. navigator.onLine only means a network interface exists, so a real request confirms connectivity.
    • Conflicts and rejections. Each operation carries a version number; a 409 shows both versions or applies a merge rule, and a 422 keeps the record visible with its error for the user to fix.
    • Limits and accounts. Storage can be evicted, so call navigator.storage.persist() and watch the quota. The queue is keyed by user, so one person's changes never replay under another account.
    What interviewers listen for
    • The service worker caches GET responses, not offline writes
    • IndexedDB outbox with client-generated ids
    • Queued means waiting, not saved
    • Replay in order; handle 409 and 422
    • Key the queue by user and watch storage limits

    Likely follow-up: Two people edited the same record offline. How would you resolve the conflict?

  47. 47.How would you migrate an NgModule-based feature to standalone components?hard

    I'd use the official schematic and go in steps. ng generate @angular/core:standalone has three modes, run in order on a path: convert declarations to standalone, remove NgModules that are no longer needed, and switch bootstrapping to bootstrapApplication. Going folder by folder keeps pull requests reviewable, with tests for key screens written first.

    Converting a component moves what its module used to supply into the component's own imports, such as NgClass or RouterLink. Components are standalone by default since Angular 19, so anything still declared in a module carries standalone: false, added by the v19 update.

    Providers need judgment. Services in a lazy module's providers had one instance per lazy module; switching them to providedIn: 'root' silently makes them app-wide singletons. To keep the same scope, list them in the lazy route's providers. Library modules that are still NgModule-based go through importProvidersFrom().

    Lazy routes change from loadChildren returning a module to returning a Routes array, or to loadComponent. Finally I'd check the lazy chunks are unchanged and deep links work.

    What interviewers listen for
    • ng generate @angular/core:standalone, step by step
    • Template dependencies move into imports
    • Keep provider scope with route providers, not root
    • importProvidersFrom() for NgModule libraries
    • Verify lazy chunks and deep links

    Likely follow-up: What does standalone: false mean in Angular 19, and why did the update add it?

  48. 48.What would you include in an Angular major-version upgrade plan, for example from 19 to 20?hard

    Prerequisites first. The update guide on angular.dev lists the steps for each version pair. Angular 20 needs TypeScript 5.8 and drops Node 18, so CI images and local Node versions may have to move before anything else. I'd also confirm that every Angular-dependent library, such as Angular Material, NgRx or a component vendor, has a release for the new major.

    Then one major at a time, on a branch: ng update @angular/core@20 @angular/cli@20, then the other Angular packages. ng update runs migrations, like the v19 one that added standalone: false, and I review those diffs instead of trusting them blindly. I also read the breaking-changes list for things no migration fixes; v20, for instance, renamed afterRender to afterEveryRender.

    Verification covers more than a green build: unit and end-to-end tests, forms, routing, SSR and hydration if used, and a bundle size comparison with the old build.

    Finally, release it like any risky change, with a staged rollout, error monitoring and a fast way to redeploy the previous build.

    What interviewers listen for
    • Check Node, TypeScript and library support first
    • One major at a time with ng update
    • Review migration diffs and breaking changes
    • Test forms, routing, SSR and bundle size
    • Staged rollout with monitoring and rollback

    Likely follow-up: What would you do if a critical third-party library doesn't support the new major yet?

  49. 49.How would you design an accessible autocomplete in Angular?hard

    I'd follow the WAI-ARIA combobox pattern, or use MatAutocomplete, which implements it. The input has role="combobox", aria-expanded and aria-controls pointing at a role="listbox". Focus stays in the input while arrow keys move a highlighted option, exposed through aria-activedescendant; Enter selects and Escape closes. Options listen to (mousedown) with preventDefault() so clicking one doesn't blur the input first.

    The query stream is the usual pipeline: trim, debounceTime(250), distinctUntilChanged(), and switchMap so stale responses are discarded. The panel has loading, 'No matches' and error states, and options use track option.id so the highlight survives a refresh.

    Announcements need restraint: one polite live-region message after results settle, like '5 suggestions', beats announcing every keystroke.

    IME input matters for Japanese, Chinese or Korean users. Angular's DefaultValueAccessor holds form updates until compositionend on desktop browsers, but a raw (input) listener doesn't, so that code should check event.isComposing. I'd test slow responses, typing during a pending request, keyboard-only use and a screen reader.

    What interviewers listen for
    • Combobox pattern with aria-activedescendant
    • Focus stays in the input; arrows, Enter and Escape
    • debounceTime and switchMap for queries
    • Loading, empty and error states; one polite announcement
    • Handle IME composition

    Likely follow-up: Why does aria-activedescendant work better here than moving real focus into the list?

  50. 50.How would you explain the architecture of a large Angular admin portal in an interview?hard

    Requirements first, because they drive everything: who uses it (support agents, finance staff, super admins), the main workflows (find a customer, issue a refund, review audit logs), how fresh the data must be, and how many teams work on it.

    Then structure. Each domain, like billing or reports, is a lazy-loaded route tree with its own signal-based services and typed API clients generated from OpenAPI. A shell provides the layout and builds the menu from the user's permissions. Shared UI sits in a library built on Angular Material and design tokens, and lint rules stop features importing each other.

    Security gets its own sentence: canMatch guards hide what a user can't use, but every endpoint authorizes on the server and sensitive actions write to an audit log.

    Then cross-cutting parts: an interceptor for 401, 403 and retries, a global ErrorHandler that reports to monitoring, server-side paging, accessibility checks, and tests for services, components and key Playwright flows.

    I'd end with one real trade-off I made, not a list of libraries.

    What interviewers listen for
    • Start with roles, workflows, data freshness and team size
    • A lazy route tree per domain with typed API clients
    • The shell builds the menu from permissions
    • Guards for UX; the server authorizes and audits
    • Interceptors, error reporting, paging and layered tests

    Likely follow-up: How would you split this portal if four teams need to release independently?

  51. 51.What is Angular, and what are the main building blocks of an Angular application?easy

    Angular is a TypeScript-based framework from Google for building single-page applications. Unlike a view library, it's batteries-included: the router, forms, HttpClient, dependency injection, testing utilities, i18n, SSR and the CLI all ship as first-party packages. It's a complete rewrite of AngularJS (1.x), not an upgrade of it.

    The main building blocks:

    • Components: a TypeScript class plus a template and styles. The UI is a tree of components.
    • Directives and pipes: add behavior to elements and format values in templates.
    • Services and dependency injection: shared logic and state, supplied by injectors.
    • Router: maps URLs to components, with lazy loading.
    • Change detection: keeps the DOM in sync with state, increasingly driven by signals.

    Templates are compiled ahead of time into efficient JavaScript instructions. Modern Angular is standalone-first: components declare their own imports, and the app starts with bootstrapApplication() instead of a root NgModule.

    What interviewers listen for
    • TypeScript framework for SPAs, batteries included
    • Components, directives, pipes, services with DI, router
    • Templates are compiled ahead of time
    • Standalone-first, with signals for reactivity
    • A rewrite of AngularJS, not a version bump

    Likely follow-up: What is the difference between a framework and a library like React?

  52. 52.What is a component in Angular, and what goes into the @Component decorator?easy

    A component is the basic UI building block: a TypeScript class that holds state and behavior, paired with a template that describes the DOM and optional styles. Angular creates an instance wherever its selector matches in a template.

    The @Component metadata tells the compiler how to treat the class:

    • selector: how it's used in templates, usually an element name like app-user-card.
    • template or templateUrl: the HTML.
    • styles or styleUrl: CSS, scoped to this component by default.
    • imports: the components, directives and pipes the template uses.
    • Optional settings such as providers, changeDetection, encapsulation and host bindings.

    In current Angular, components are standalone by default, so each one declares its own template dependencies instead of relying on an NgModule's declarations. Keep the class focused on presentation; data access and shared state belong in services.

    What interviewers listen for
    • Class for state and behavior, template for the view
    • selector, template, styles, imports are the core metadata
    • Standalone by default: imports lists template dependencies
    • Keep data access in services, not components

    Likely follow-up: What kinds of selectors can a component use?

  53. 53.What types of data binding does Angular support?easy

    Data flows between the class and the template in four ways:

    • Interpolation {{ expr }}: one-way, class to view. Renders the value as text.
    • Property binding [prop]="expr": one-way, class to view. Sets a DOM or component property, e.g. [disabled]="saving". Variants: [attr.aria-expanded] for attributes, [class.active] and [style.width.px].
    • Event binding (event)="handler($event)": one-way, view to class, for DOM events and component outputs.
    • Two-way binding [(value)]="x": sugar for a property binding plus a valueChange event binding, as used by [(ngModel)] or a model() input.

    A detail interviewers like: property binding targets DOM properties, not HTML attributes. Attributes only initialize properties, so an attribute with no matching property, such as data-*, is bound with [attr.name]. Template expressions should be cheap and side-effect free, because they're re-evaluated during change detection.

    <h2>{{ title }}</h2>
    <img [src]="avatarUrl" [alt]="name" />
    <button
      [attr.aria-expanded]="open"
      [class.active]="open"
      (click)="toggle()">
      Menu
    </button>
    <input [(ngModel)]="query" />
    What interviewers listen for
    • Interpolation and property binding: class to view
    • Event binding: view to class
    • Two-way [()] is property binding plus a Change event
    • Property bindings set DOM properties; use [attr.x] for attributes

    Likely follow-up: What is the difference between an HTML attribute and a DOM property?

  54. 54.What are directives, and what is the difference between structural and attribute directives?easy

    A directive is a class that adds behavior to elements in a template. There are three kinds:

    • Components: directives that have their own template.
    • Attribute directives change the appearance or behavior of the element they're on without adding or removing it: ngClass, ngStyle, ngModel, or a custom appHighlight.
    • Structural directives change the DOM layout by creating and destroying views: *ngIf, *ngFor, *ngSwitchCase. The asterisk is sugar: Angular wraps the element in an <ng-template>, and the directive uses TemplateRef and ViewContainerRef to decide when to render it.

    Only one structural directive can sit on an element; nest them with <ng-container>. In modern templates, the built-in control flow blocks @if, @for and @switch replace the common structural directives: they're part of the template syntax, need no import, and NgIf/NgFor are now deprecated. Custom structural directives are still useful for things like permission checks.

    What interviewers listen for
    • Components are directives with a template
    • Attribute directives modify an existing element
    • Structural directives add or remove DOM via ng-template
    • * is sugar for <ng-template>; one per element
    • @if/@for/@switch replace *ngIf/*ngFor/ngSwitch

    Likely follow-up: How would you write a custom structural directive?

  55. 55.What are the component lifecycle hooks, and in what order are they called?easy

    Lifecycle hooks are methods Angular calls at specific moments. After the constructor, the order is:

    • ngOnChanges: before ngOnInit when the component has bound inputs, then every time an input changes. Receives SimpleChanges.
    • ngOnInit: once, after the first inputs are set. Initialization logic goes here.
    • ngDoCheck: on every change detection run, for custom change checks.
    • ngAfterContentInit, then ngAfterContentChecked: projected content is initialized, then checked.
    • ngAfterViewInit, then ngAfterViewChecked: the component's view and its child views are initialized (view queries resolved), then checked.
    • ngOnDestroy: once, before the component is destroyed. Clean up here.

    On later passes only ngOnChanges (if an input changed), ngDoCheck and the two Checked hooks run again. Modern additions: inject(DestroyRef).onDestroy() registers cleanup from anywhere in an injection context, and afterNextRender()/afterEveryRender() are for DOM work and run only in the browser.

    What interviewers listen for
    • constructor, ngOnChanges, ngOnInit, ngDoCheck
    • Content hooks run before view hooks
    • Init hooks run once; Checked hooks every pass
    • ngOnDestroy or DestroyRef for cleanup
    • afterNextRender for browser-only DOM work

    Likely follow-up: Why would ngOnChanges not fire when you mutate an input object?

  56. 56.What is the difference between the constructor and ngOnInit?easy

    The constructor is a plain TypeScript class feature. Angular calls it when it instantiates the component, before any inputs are bound. It's the place to inject dependencies and set simple defaults, and nothing that depends on inputs: reading a required input() there throws, and a classic @Input() still holds its default or undefined.

    ngOnInit is an Angular lifecycle hook that runs once, after the first ngOnChanges, when input values are available. Initialization that depends on inputs belongs there, like starting a fetch based on an id input.

    Rules of thumb:

    • Keep constructors light. Heavy work there is harder to test and runs before Angular has finished setting the component up.
    • The view doesn't exist yet in either one; use ngAfterViewInit or afterNextRender() for DOM access.
    • With signals, many ngOnInit jobs become a computed() derived from inputs, or an effect() created in the constructor.
    What interviewers listen for
    • Constructor: class feature, runs before inputs are set
    • ngOnInit: Angular hook, runs once after first inputs
    • Constructor for injection; ngOnInit for input-dependent setup
    • Neither has the rendered view yet
  57. 57.What is dependency injection in Angular, and how does it work?mid

    Dependency injection is a pattern where a class declares what it needs and the framework supplies it, instead of the class constructing its dependencies itself. In Angular you request a token (usually a class) with inject(UserService) or a constructor parameter, and an injector finds a provider that says how to create the value.

    Providers are registered in a few places:

    • @Injectable({ providedIn: 'root' }) for an app-wide singleton.
    • The providers array of the ApplicationConfig, a route, or a component.

    Injectors form a hierarchy. Angular first walks the element injectors up the component tree, then the environment injectors (route, root, platform). The first provider found wins, and its injector owns the instance. If nothing is found, you get a no-provider error unless the dependency was marked optional.

    The payoff is loose coupling, easy testing (swap a real service for a fake in TestBed), and control over each dependency's scope and lifetime.

    What interviewers listen for
    • Classes ask for tokens; injectors resolve them via providers
    • Register with providedIn, app config, route or component providers
    • Lookup: element injectors first, then environment injectors
    • First provider found wins; missing provider throws
    • Improves testability and decoupling

    Likely follow-up: What does providedIn: 'root' do? · How can the same service end up with two instances?

  58. 58.What is a service in Angular, and what does providedIn: 'root' do?easy

    A service is a class for logic that doesn't belong to one component's view: fetching data, shared state, logging, auth. It's marked with @Injectable() so Angular can create it and inject its own dependencies.

    @Injectable({ providedIn: 'root' }) registers the service with the root environment injector, which gives you:

    • One instance for the whole app, shared by everything that injects it.
    • Lazy creation: it's instantiated the first time something injects it.
    • Tree-shaking: if nothing injects it, it's dropped from the bundle. A class listed in a providers array is always bundled.

    "Singleton" really means one instance per injector. If you also list the service in a component's providers, that component and its children get their own separate instance, and a route's providers does the same for that route's subtree. That's handy for deliberately scoped state, but it's also the usual cause of "my service state keeps resetting".

    What interviewers listen for
    • Service: injectable class for shared logic or state
    • providedIn: 'root': one lazy, tree-shakable app-wide instance
    • A singleton is only single per injector
    • Component or route providers create extra instances
  59. 59.What is the difference between an Observable and a Promise?easy
    • Values: a Promise settles once, with one value or an error. An Observable can emit zero, one or many values over time, then complete or error.
    • Laziness: a Promise's executor runs as soon as it's created. An Observable does nothing until you subscribe, and a cold Observable runs its producer again for each subscriber. That's why subscribing twice to http.get() sends two requests.
    • Cancellation: a Promise can't be cancelled. Unsubscribing from an Observable runs its teardown logic, and HttpClient aborts the request.
    • Timing: Promise callbacks always run asynchronously, as microtasks. Observables can emit synchronously (of(1)) or asynchronously.
    • Composition: Observables come with operators such as map, switchMap, retry and debounceTime.

    Angular uses Observables for HttpClient, router events and form valueChanges. To bridge the two, firstValueFrom() and lastValueFrom() turn an Observable into a Promise, and from(promise) goes the other way.

    What interviewers listen for
    • Promise: one value; Observable: zero to many over time
    • Promise is eager; Observable is lazy until subscribed
    • Observables can be cancelled by unsubscribing
    • Operators compose streams; firstValueFrom bridges to Promises

    Likely follow-up: What is the difference between a hot and a cold Observable?

  60. 60.How does change detection work in Angular?mid

    Change detection keeps the DOM in sync with component state. Each compiled template has an update function that re-evaluates its bindings, compares each result with the previous value (by identity, not deep equality), and touches only the DOM that changed. A pass walks the component tree top-down from the root, so parents are checked before their children.

    Two questions matter: when a pass runs, and which components it checks.

    • When: classically, Zone.js patched async APIs (events, timers, promises, XHR) and triggered ApplicationRef.tick() after each task. Zoneless apps, the default in recent versions, schedule a pass only when Angular is notified: a signal read in a template changes, a template listener fires, markForCheck() is called (the async pipe does this), or an input is set.
    • Which: the eager strategy checks every component on every pass. OnPush components are checked only when marked dirty, and signals let Angular refresh just the views that read the changed signal.

    In development mode Angular runs an extra check and throws NG0100 (ExpressionChangedAfterItHasBeenChecked) if a binding changed in between.

    What interviewers listen for
    • Re-evaluates template bindings and patches changed DOM
    • Top-down traversal; values compared by identity
    • Zone.js triggered passes; zoneless relies on notifications
    • OnPush and signals narrow which views get checked

    Likely follow-up: What does OnPush change? · What triggers change detection in a zoneless app?

  61. 61.How do Angular components communicate with each other?easy

    It depends on how the components are related:

    • Parent to child: inputs. The child declares input() (classic @Input()) and the parent binds [todo]="item".
    • Child to parent: outputs. The child declares output() (classic @Output() with an EventEmitter) and calls emit(); the parent listens with (toggled)="onToggle($event)".
    • Both ways: model() enables [(value)] two-way binding.
    • Parent calling into a child: a view query such as viewChild(TodoItem), or a template reference variable, gives the parent the child instance.
    • Siblings or unrelated components: a shared service that holds state in signals or a BehaviorSubject, provided at the right level.
    • Across routes: route params, query params and navigation state.

    Content projection with <ng-content> is the other composition tool: the parent passes markup instead of data. When lots of state is shared across a large app, a store (NgRx or a signal-based store) formalizes the service approach.

    What interviewers listen for
    • Parent to child: input() or @Input()
    • Child to parent: output() or @Output() events
    • Parent reaches a child instance with viewChild() or a template ref
    • Unrelated components share a service with signals or subjects

    Likely follow-up: When would you reach for a state management library instead of a service?

  62. 62.What are pipes, what is the difference between pure and impure pipes, and how do you create a custom one?easy

    Pipes transform values for display in templates: {{ price | currency:'EUR' }}, {{ createdAt | date:'short' }}. Built-ins include date, currency, decimal, percent, uppercase, titlecase, slice, json, keyvalue and async.

    • Pure pipes (the default) re-run transform only when the input value or an argument changes: a different primitive or a new object reference. Mutating an array in place doesn't re-run them. That makes them cheap, effectively memoized per binding.
    • Impure pipes (pure: false) run on every change detection pass, so they must be fast. async, json and keyvalue are impure because their output can change while the input reference stays the same.

    A custom pipe is a class decorated with @Pipe({ name }) that implements PipeTransform. Pipes are standalone by default, so you add them to a component's imports. Prefer a pure pipe (or a computed() signal) over calling a method in the template, which re-runs on every check.

    @Pipe({ name: 'truncate' })
    export class TruncatePipe implements PipeTransform {
      transform(value: string, limit = 20): string {
        return value.length > limit ? value.slice(0, limit) + '…' : value;
      }
    }
    
    // In a component with imports: [TruncatePipe]
    // {{ post.title | truncate:40 }}
    What interviewers listen for
    • Pipes format values in templates with |
    • Pure (default): re-run only when input or args change by reference
    • Impure: run every change detection pass (async, json)
    • Custom: @Pipe + PipeTransform, then add to imports

    Likely follow-up: Why does a pure pipe not update when you push to an array?

  63. 63.What are standalone components, and how do they differ from NgModules?easy

    NgModules were Angular's original unit of compilation. A module listed components in declarations, pulled in dependencies through imports, shared things with exports and registered providers. A component could only use what its module made available, which led to lots of indirection and "shared modules".

    A standalone component lists its own template dependencies in its imports array, so it's self-contained. It's the default in current Angular, and the rest of the app follows the same style:

    • Bootstrap with bootstrapApplication(App, appConfig) instead of bootstrapModule(AppModule).
    • Configure with provider functions such as provideRouter(routes) and provideHttpClient() instead of RouterModule.forRoot() and HttpClientModule.
    • Lazy load a single component with loadComponent.

    The benefits are less boilerplate, dependencies visible in one place, and simpler lazy loading and testing. The two styles interoperate: a standalone component can import an NgModule, and an NgModule can import a standalone component. The CLI ships a migration: ng generate @angular/core:standalone.

    What interviewers listen for
    • NgModules group declarations, imports, exports, providers
    • Standalone components declare their own imports
    • Standalone is the default in current Angular
    • bootstrapApplication and provide*() functions replace root modules
    • Both styles interoperate; the CLI can migrate
  64. 64.What does the OnPush change detection strategy do, and when does an OnPush component update?mid

    With ChangeDetectionStrategy.OnPush, Angular checks a component only when its view has been marked dirty. Otherwise the traversal skips it and, unless something inside was flagged, its whole subtree. A component is marked dirty when:

    • an input receives a new reference (via a template binding or setInput()),
    • an event handled by a template or host listener fires in the component or a descendant,
    • the async pipe receives a value,
    • markForCheck() is called,
    • a signal read in its template changes.

    What does not update it: mutating an input object in place, or assigning a plain field inside a setTimeout, a promise or a subscribe callback. That's why OnPush code uses immutable updates (spread, map, filter) and signals or the async pipe for async data.

    The payoff is fewer checks in large trees and a more predictable data flow. In the newest Angular versions OnPush is the default when changeDetection isn't set, and the check-always strategy is named Eager (previously Default).

    @Component({
      selector: 'app-user-card',
      changeDetection: ChangeDetectionStrategy.OnPush,
      template: `<h3>{{ user().name }}</h3>`,
    })
    export class UserCard {
      readonly user = input.required<User>();
    }
    
    // In the parent, bound as [user]="user":
    this.user.name = 'Grace';                    // same reference: card stays stale
    this.user = { ...this.user, name: 'Grace' }; // new reference: card updates
    What interviewers listen for
    • Checked only when marked dirty; clean subtrees are skipped
    • Dirty on new input reference, handled event, async pipe, signal
    • Mutating inputs in place does not update the view
    • Pairs with immutable data and signals

    Likely follow-up: How do you update an OnPush component from a setTimeout callback?

  65. 65.What is the difference between switchMap, mergeMap, concatMap and exhaustMap?mid

    All four map each source value to an inner Observable, subscribe to it and flatten its values into one stream. They differ only in what happens when a new value arrives while the previous inner Observable is still running:

    • switchMap cancels the previous inner and switches to the new one. Use it when only the latest result matters: typeahead search, loading details for the current route param.
    • mergeMap runs inners concurrently, optionally with a concurrency limit. Use it for independent parallel work where order doesn't matter.
    • concatMap queues values and runs inners one at a time, in order. Use it for sequential writes such as autosave.
    • exhaustMap ignores new values until the current inner completes. Use it to prevent double submits on a login or save button.

    Two points that impress interviewers: don't use switchMap for writes, because unsubscribing doesn't undo a request the server already received; and put catchError on the inner Observable, so one failed request doesn't kill the outer stream.

    results$ = this.query.valueChanges.pipe(
      debounceTime(300),
      distinctUntilChanged(),
      switchMap((q) => this.api.search(q).pipe(catchError(() => of([])))),
    );
    
    saved$ = this.drafts$.pipe(concatMap((draft) => this.api.save(draft)));
    
    loggedIn$ = this.submit$.pipe(exhaustMap(() => this.auth.login(this.form.getRawValue())));
    What interviewers listen for
    • All flatten inner Observables; they differ on overlap
    • switchMap cancels the previous inner: typeahead, route params
    • mergeMap runs in parallel; concatMap queues in order
    • exhaustMap ignores new values while busy: double submits
    • Avoid switchMap for writes; catch errors inside the inner

    Likely follow-up: What happens to an HTTP request when switchMap unsubscribes from it?

  66. 66.What is the difference between Subject, BehaviorSubject, ReplaySubject and AsyncSubject?mid

    All four are both an Observable and an Observer, and they're multicast: every subscriber shares the same execution. The difference is what a late subscriber receives:

    • Subject: no initial value and no memory. Subscribers only get values emitted after they subscribe.
    • BehaviorSubject: requires an initial value and always holds the current one. A new subscriber immediately gets the latest value, and you can read it synchronously with .value or getValue(). The classic choice for state in services.
    • ReplaySubject(bufferSize, windowTime): no initial value; replays the last N values (optionally only those within a time window) to each new subscriber.
    • AsyncSubject: emits only its last value, and only when it completes. Subscribers that arrive after completion get that value too, a bit like a Promise.

    Expose subjects from services through asObservable() so consumers can't call next(). In modern Angular, a signal() often replaces a BehaviorSubject for synchronous state.

    const s = new Subject<number>();
    s.next(1);
    s.subscribe(console.log); // logs 2 only: 1 was emitted before subscribing
    s.next(2);
    
    const b = new BehaviorSubject(0);
    b.next(1);
    b.subscribe(console.log); // logs 1 immediately, then 2
    b.next(2);
    
    const r = new ReplaySubject<number>(2);
    r.next(1); r.next(2); r.next(3);
    r.subscribe(console.log); // logs 2, 3
    What interviewers listen for
    • Subject: late subscribers get only future values
    • BehaviorSubject: initial value, replays current value, .value
    • ReplaySubject: replays the last N values
    • AsyncSubject: emits the last value on completion
    • Expose via asObservable()

    Likely follow-up: When would you use a signal instead of a BehaviorSubject?

  67. 67.How do you set up routing in an Angular application?easy

    You define a Routes array and register it with provideRouter(routes) in the application config (classic apps use RouterModule.forRoot(routes)). Each route maps a path to a component or to a lazy loadComponent/loadChildren, and can have children, redirectTo, guards, resolvers and data.

    In templates:

    • <router-outlet /> marks where the routed component renders.
    • routerLink="/users" or [routerLink]="['/users', id]" navigates without a full page reload, and routerLinkActive adds a class to the active link.

    In code, inject(Router).navigate(['/users', id]) navigates, and ActivatedRoute exposes paramMap, queryParamMap and data, as Observables or through snapshot. With withComponentInputBinding(), params, query params and resolved data are bound straight to component inputs.

    Order matters: the router takes the first route that matches, so put specific paths before general ones and the ** wildcard last. An empty-path redirect needs pathMatch: 'full'.

    export const routes: Routes = [
      { path: '', redirectTo: 'users', pathMatch: 'full' },
      { path: 'users', component: UserList },
      { path: 'users/:id', component: UserDetail },
      { path: 'admin', loadChildren: () => import('./admin/admin.routes').then((m) => m.ADMIN_ROUTES) },
      { path: '**', component: NotFound },
    ];
    
    bootstrapApplication(App, {
      providers: [provideRouter(routes, withComponentInputBinding())],
    });
    What interviewers listen for
    • Routes array registered with provideRouter()
    • <router-outlet> renders; routerLink navigates
    • ActivatedRoute or input binding for params
    • First match wins; wildcard ** goes last

    Likely follow-up: What is the difference between paramMap and snapshot.paramMap?

  68. 68.What is the difference between template-driven and reactive forms, and when would you use each?easy

    Both are built on the same model classes (FormControl, FormGroup), but they differ in where the form model lives.

    • Template-driven (FormsModule): you describe the form in the template with ngModel and validation attributes such as required and minlength, and directives create the controls implicitly. The data model is mutable and bound two-way, and setup is asynchronous because the controls are created as the view renders. Good for simple forms: login, a search box, a settings toggle.
    • Reactive (ReactiveFormsModule): you build the model explicitly in the class with FormGroup, FormControl, FormArray or FormBuilder, and bind it with [formGroup] and formControlName. Access is synchronous, values are strongly typed, valueChanges and statusChanges are Observables, and validators are plain functions. That makes them easier to unit test and better for dynamic or complex forms.

    Most teams default to reactive forms for anything non-trivial. Recent Angular versions also add Signal Forms, a signal-based API in @angular/forms/signals.

    What interviewers listen for
    • Template-driven: ngModel, model implicit in the template
    • Reactive: explicit model in the class, bound with [formGroup]
    • Reactive is synchronous, typed and easier to test
    • Template-driven suits simple forms; reactive suits complex ones

    Likely follow-up: How do you add a field dynamically in a reactive form?

  69. 69.What are route guards, and what types does Angular provide?mid

    Guards are functions the router runs to decide whether a navigation may proceed. Modern Angular uses functional guards, which can call inject():

    • canActivate: may the user enter this route? (auth, roles)
    • canActivateChild: the same check for every child route.
    • canDeactivate: may the user leave? Typically "you have unsaved changes".
    • canMatch: may this route definition be used at all? If it returns false, the router tries the next matching route, and a lazy route's code isn't downloaded. It replaces the deprecated canLoad.

    A guard returns true/false, a UrlTree or RedirectCommand to redirect, or an Observable or Promise of one of those. Returning a UrlTree is better than calling router.navigate() inside the guard, because the router handles the redirect as part of the same navigation.

    Class-based guards implementing CanActivate still work, but functions are simpler and composable. And guards are a UX feature, not security: the server must still enforce authorization.

    export const authGuard: CanActivateFn = () => {
      const auth = inject(AuthService);
      return auth.isLoggedIn() || inject(Router).createUrlTree(['/login']);
    };
    
    export const unsavedChangesGuard: CanDeactivateFn<EditPage> = (page) =>
      !page.dirty() || confirm('Discard unsaved changes?');
    
    export const routes: Routes = [
      { path: 'settings', component: EditPage, canActivate: [authGuard], canDeactivate: [unsavedChangesGuard] },
    ];
    What interviewers listen for
    • canActivate, canActivateChild, canDeactivate, canMatch
    • Functional guards use inject() for dependencies
    • Return boolean, UrlTree/RedirectCommand, or async versions
    • canMatch replaces canLoad and skips lazy downloads
    • Guards are UX; the server enforces security

    Likely follow-up: In what order do guards and resolvers run?

  70. 70.How does lazy loading work in Angular, and how do you lazy load a route?mid

    Lazy loading splits the app into chunks that are downloaded only when needed, which shrinks the initial bundle and speeds up the first load. The build creates a separate chunk for every dynamic import().

    For routes:

    • loadComponent: () => import('./settings/settings').then((m) => m.Settings) lazy loads one standalone component. If the file uses a default export, () => import('./settings/settings') is enough.
    • loadChildren: () => import('./admin/admin.routes').then((m) => m.ADMIN_ROUTES) lazy loads a whole set of child routes (in NgModule apps, a routing module).

    Related tools:

    • Preloading: withPreloading(PreloadAllModules) downloads lazy chunks in the background after the app starts, so later navigations are instant.
    • canMatch guards keep a chunk from being downloaded for users who can't use it.
    • Route providers give a lazy section its own environment injector and scoped services.
    • @defer lazy loads parts of a template rather than whole routes.

    Don't statically import a lazy component elsewhere, or it ends up back in the main bundle.

    What interviewers listen for
    • Dynamic import() creates separate chunks
    • loadComponent for one component, loadChildren for route sets
    • Preloading downloads chunks in the background
    • canMatch prevents downloading unauthorized chunks
    • @defer lazy loads template sections

    Likely follow-up: How would you write a custom preloading strategy?

  71. 71.What are signals in Angular? Explain signal, computed and effect.mid

    A signal is a reactive wrapper around a value that notifies its consumers when the value changes. You read it by calling it, count(), which lets Angular track who depends on it.

    • signal(initial) creates a writable signal, changed with set() or update(fn). asReadonly() exposes a read-only view of it.
    • computed(() => ...) derives a read-only value. It's lazy and memoized: it recalculates only after a signal it read changes, and it tracks dependencies dynamically.
    • effect(() => ...) runs a side effect whenever the signals it read change. It runs at least once, runs asynchronously as part of change detection, needs an injection context, and is destroyed with the component or service that created it. Use it to sync with the outside world (storage, logging, a chart library), not to copy one signal into another; that's a computed().

    Signals compare with Object.is by default, so update immutably. Templates are reactive consumers too: a signal change refreshes only the views that read it, which makes OnPush and zoneless change detection practical. untracked() reads a signal without tracking it.

    export class Cart {
      readonly items = signal<Item[]>([]);
      readonly total = computed(() => this.items().reduce((sum, i) => sum + i.price, 0));
    
      constructor() {
        effect(() => localStorage.setItem('cart', JSON.stringify(this.items())));
      }
    
      add(item: Item) {
        this.items.update((list) => [...list, item]); // new array, not push()
      }
    }
    What interviewers listen for
    • signal() holds writable state; read it by calling it
    • computed() is lazy, memoized and read-only
    • effect() is for side effects, not for deriving state
    • Object.is equality by default: update immutably
    • Only views that read a changed signal are refreshed

    Likely follow-up: How do signals differ from RxJS Observables? · What is linkedSignal for?

  72. 72.What are HTTP interceptors, and how do you write one in modern Angular?mid

    An interceptor is middleware for HttpClient: every request passes through the chain on the way out, and every response on the way back. Typical uses are auth headers, logging, global error handling, retries, caching and loading indicators.

    In modern Angular an interceptor is a function of type HttpInterceptorFn. It receives the request and a next handler and returns next(req), optionally transformed with RxJS operators. It runs in an injection context, so it can call inject() (at the top, not inside async callbacks). Register it with provideHttpClient(withInterceptors([...])).

    Key details:

    • Requests are immutable: use req.clone({ setHeaders: {...} }) to change one.
    • Order matters: requests flow through interceptors in the listed order, responses in reverse.
    • HttpContext tokens pass per-request flags, such as "skip auth for this call".

    Class-based interceptors (implementing HttpInterceptor, registered with the HTTP_INTERCEPTORS multi-provider) still work, but need withInterceptorsFromDi().

    export const authInterceptor: HttpInterceptorFn = (req, next) => {
      const token = inject(AuthService).token();
      const router = inject(Router);
      const authReq = token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req;
    
      return next(authReq).pipe(
        catchError((err: HttpErrorResponse) => {
          if (err.status === 401) router.navigate(['/login']);
          return throwError(() => err);
        }),
      );
    };
    
    // app.config.ts: provideHttpClient(withInterceptors([authInterceptor]))
    What interviewers listen for
    • Middleware around every HttpClient request and response
    • Functional HttpInterceptorFn registered via withInterceptors
    • Requests are immutable: modify with req.clone()
    • Request order as listed; responses in reverse

    Likely follow-up: How would you retry failed requests or refresh an expired token in an interceptor?

  73. 73.How do you prevent memory leaks from RxJS subscriptions in Angular?mid

    A subscription to a long-lived Observable keeps running after the component is destroyed: its callback still fires and holds a reference to the component, so it can't be garbage collected. Typical culprits are interval, fromEvent, subjects in root services, store selectors, router events and valueChanges. HTTP calls complete after one response, so they leak less, but cancelling them on destroy still avoids work for a dead component.

    Options, most preferred first:

    • Don't subscribe manually: the async pipe and toSignal() both unsubscribe automatically.
    • takeUntilDestroyed() from @angular/core/rxjs-interop completes the stream when the component, directive or service is destroyed. It needs an injection context; in ngOnInit or later, pass it a DestroyRef.
    • take(1) or first() when you only need one value.
    • Classic: a destroy$ Subject used with takeUntil(this.destroy$) and completed in ngOnDestroy, or keeping the Subscription and calling unsubscribe().

    Put takeUntil or takeUntilDestroyed last in the pipe, so no later operator keeps an inner subscription alive.

    export class Dashboard implements OnInit {
      private readonly destroyRef = inject(DestroyRef);
      private readonly route = inject(ActivatedRoute);
    
      constructor() {
        interval(5000).pipe(takeUntilDestroyed()).subscribe(() => this.refresh());
      }
    
      ngOnInit() {
        this.route.paramMap
          .pipe(takeUntilDestroyed(this.destroyRef)) // not an injection context: pass DestroyRef
          .subscribe((params) => this.load(params.get('id')));
      }
    }
    What interviewers listen for
    • Long-lived subscriptions outlive the component and leak
    • Prefer async pipe or toSignal(): automatic cleanup
    • takeUntilDestroyed() needs an injection context or DestroyRef
    • Classic: takeUntil(destroy$) last in the pipe

    Likely follow-up: Do you need to unsubscribe from HttpClient calls?

  74. 74.What does the async pipe do, and why is it preferred over subscribing manually?easy

    The async pipe subscribes to an Observable (or a Promise) from the template and returns its latest value. It:

    • Unsubscribes automatically when the component is destroyed, or when the bound expression switches to a different Observable, so there's no leak.
    • Marks the component for check on every emission, so it works with OnPush components.
    • Returns null until the first value arrives.

    It's preferred because there's less boilerplate, no manual subscription management, and the data stays a declarative stream instead of being copied into mutable fields.

    The main pitfall: each | async creates its own subscription. Writing user$ | async three times against a cold HTTP Observable sends three requests. Subscribe once with @if (user$ | async; as user) or @let user = user$ | async;, or share the stream with shareReplay. In signal-based code, toSignal() does the same job from the class.

    What interviewers listen for
    • Subscribes in the template and returns the latest value
    • Unsubscribes automatically on destroy
    • Calls markForCheck(), so it works with OnPush
    • Each | async is a separate subscription: subscribe once
  75. 75.What happens when an Angular application starts? How is it bootstrapped?easy

    The CLI build uses main.ts as the entry point, and index.html contains the root element, such as <app-root>.

    In a standalone app, main.ts calls bootstrapApplication(App, appConfig). Angular then:

    • creates the platform and the root environment injector from the providers in appConfig (provideRouter, provideHttpClient and so on); providedIn: 'root' services are created on demand later,
    • runs app initializers registered with provideAppInitializer() and waits for any async ones,
    • creates the root component, renders it into the element that matches its selector, and starts change detection so later state changes reach the DOM.

    In NgModule apps, main.ts calls platformBrowser().bootstrapModule(AppModule) (older code uses platformBrowserDynamic()), and AppModule lists the root component in its bootstrap array.

    With SSR, the server renders the same app to HTML first, and the browser hydrates that markup instead of rendering from scratch.

    What interviewers listen for
    • main.ts calls bootstrapApplication(App, appConfig)
    • Root injector is built from appConfig providers
    • App initializers run before the root component renders
    • NgModule apps use bootstrapModule(AppModule)
  76. 76.What is Angular's built-in control flow (@if, @for, @switch), and how does it differ from *ngIf and *ngFor?easy

    Built-in control flow is template syntax for conditionals and loops that replaces the structural directives:

    • @if (cond) { } @else if (other) { } @else { }, with as to alias the value, e.g. @if (user$ | async; as user).
    • @for (item of items; track item.id) { } @empty { }, with the implicit variables $index, $count, $first, $last, $even and $odd.
    • @switch (status) { @case ('active') { } @default { } }.

    How it differs from *ngIf, *ngFor and ngSwitch:

    • It's built into the compiler, so there's nothing to import and less runtime overhead.
    • track is required in @for, which forces you to think about item identity; the old trackBy was optional and often forgotten.
    • @empty and @else if are built in, and type narrowing works better inside @if.
    • No <ng-template> or <ng-container> wrappers are needed.

    NgIf, NgFor and NgSwitch are deprecated, and the CLI can migrate templates with ng generate @angular/core:control-flow.

    @if (loading()) {
      <app-spinner />
    } @else {
      @for (user of users(); track user.id; let i = $index) {
        <p>{{ i + 1 }}. {{ user.name }}</p>
      } @empty {
        <p>No users found.</p>
      }
    }
    What interviewers listen for
    • @if, @for, @switch are built into the template syntax
    • No imports needed; less overhead than directives
    • track is required in @for; @empty is built in
    • NgIf/NgFor/NgSwitch are deprecated; CLI migration exists

    Likely follow-up: What should you pass to track, and what happens if you track by $index?

  77. 77.What are input(), output() and model(), and how do they compare with @Input() and @Output()?easy

    They're the signal-based way to declare a component's public API.

    • input<T>(defaultValue) and input.required<T>() return a read-only InputSignal. You read it as this.name() in the class and name() in the template, and derive from it with computed() instead of ngOnChanges. Options include alias and transform, e.g. transform: booleanAttribute.
    • output<T>() returns an OutputEmitterRef: call this.rated.emit(value), and the parent listens with (rated)="...". Outputs don't bubble like DOM events.
    • model<T>() is a writable input signal for two-way binding. When the component calls set() or update(), Angular emits a matching <name>Change event, so the parent can write [(value)]="score".

    The classic equivalents are @Input() name!: string, with ngOnChanges or a setter to react to changes, and @Output() rated = new EventEmitter<number>(). The new APIs add compile-time checking of required inputs, better type inference and built-in reactivity through computed and effect, and they fit naturally with OnPush and zoneless change detection.

    @Component({
      selector: 'app-rating',
      template: `
        @for (star of stars; track star) {
          <button [disabled]="disabled()" (click)="value.set(star); rated.emit(star)">★</button>
        }
      `,
    })
    export class Rating {
      readonly value = model(0); // parent: [(value)]="score"
      readonly disabled = input(false, { transform: booleanAttribute });
      readonly rated = output<number>();
      protected readonly stars = [1, 2, 3, 4, 5];
    }
    What interviewers listen for
    • input() returns a read-only signal; input.required() for mandatory
    • output() emits events with .emit(); no bubbling
    • model() is a writable input enabling [(x)]
    • Replace ngOnChanges with computed() on inputs

    Likely follow-up: What does an input transform do?

  78. 78.What is content projection, and how does <ng-content> work?mid

    Content projection lets a parent pass markup into a child component, much like children in React. The child's template marks where it goes with <ng-content>.

    • Single slot: <ng-content /> renders everything the parent put between the child's tags.
    • Multi-slot: <ng-content select="[card-title]" /> takes only matching content (a CSS selector on element, attribute or class), and a plain <ng-content /> catches the rest. ngProjectAs lets content match a different selector.
    • Fallback content: markup inside <ng-content> renders when nothing is projected into that slot.

    A key detail: projected content is created and owned by the parent. It's instantiated even if the child hides the <ng-content>, so wrapping it in @if doesn't prevent creation, and the same content can't be projected twice. For conditional, lazy or repeated rendering, the parent passes an <ng-template> and the child renders it with ngTemplateOutlet.

    The child reaches projected components with contentChild()/contentChildren() (classic @ContentChild), ready by ngAfterContentInit.

    @Component({
      selector: 'app-card',
      template: `
        <header><ng-content select="[card-title]" /></header>
        <section><ng-content /></section>
        <footer><ng-content select="app-card-actions">No actions</ng-content></footer>
      `,
    })
    export class Card {}
    
    // <app-card>
    //   <h2 card-title>Invoice #42</h2>
    //   <p>Due next week.</p>
    // </app-card>
    What interviewers listen for
    • <ng-content> is a slot for markup passed by the parent
    • select creates named slots; default slot catches the rest
    • Projected content is always created, even if hidden
    • Use ng-template + ngTemplateOutlet for conditional or repeated content

    Likely follow-up: How do you access a projected component from the child?

  79. 79.What is the difference between <ng-template>, <ng-container> and <ng-content>?mid
    • <ng-template> defines a template fragment that is not rendered on its own. Something has to instantiate it: a structural directive, ngTemplateOutlet, the else of *ngIf, or ViewContainerRef.createEmbeddedView(). It can declare context variables with let-, so one fragment can render with different data, and a template reference such as #row gives you its TemplateRef.
    • <ng-container> is a grouping element that renders no DOM node. Use it to apply a structural directive without a wrapper <div>, to nest structural directives (only one is allowed per element), or as the host for *ngTemplateOutlet.
    • <ng-content> is a projection slot in a child component's template, where the parent's markup is inserted.

    A common pattern combines them: a reusable list or table component accepts a TemplateRef (as an input, or through contentChild(TemplateRef)) so the parent decides how each item looks, and the component renders it with ngTemplateOutlet plus a context object.

    <ng-template #userRow let-user let-i="index">
      <li>{{ i + 1 }}. {{ user.name }}</li>
    </ng-template>
    
    <ul>
      @for (u of users(); track u.id; let i = $index) {
        <ng-container *ngTemplateOutlet="userRow; context: { $implicit: u, index: i }" />
      }
    </ul>
    What interviewers listen for
    • ng-template: a fragment rendered only when instantiated
    • let- variables receive values from the context object
    • ng-container: grouping without an extra DOM element
    • ng-content: slot for projected parent markup
  80. 80.What is the difference between ViewChild and ContentChild, and when are their results available?mid

    Both are queries that give a component a reference to something rendered in its tree. They differ in where they look:

    • View queries (viewChild(), viewChildren(), classic @ViewChild/@ViewChildren) search the component's own template.
    • Content queries (contentChild(), contentChildren(), classic @ContentChild/@ContentChildren) search the projected content the parent passed in through <ng-content>.

    You can query by template reference name ('box'), by component or directive class, or by token, and pick what you get back with read (ElementRef, TemplateRef, ViewContainerRef).

    Timing: with decorators, view results are ready in ngAfterViewInit and content results in ngAfterContentInit, or already in ngOnInit with static: true if the element isn't inside a conditional block. The plural forms return a QueryList with a changes Observable. Signal queries return signals that update automatically, so you read them in computed(), effect() or afterNextRender() without worrying about hook timing, and viewChild.required() removes the undefined case.

    @Component({
      selector: 'app-search',
      template: `<input #box /><ng-content />`,
    })
    export class Search {
      readonly box = viewChild.required<ElementRef<HTMLInputElement>>('box');
      readonly tabs = contentChildren(Tab); // projected <app-tab> components
    
      constructor() {
        afterNextRender(() => this.box().nativeElement.focus());
      }
    }
    What interviewers listen for
    • View queries search the own template
    • Content queries search projected content
    • Decorators: ready in ngAfterViewInit / ngAfterContentInit
    • Signal queries (viewChild()) update automatically
    • read chooses ElementRef, TemplateRef or ViewContainerRef
  81. 81.What is the difference between AOT and JIT compilation in Angular?easy

    Angular templates aren't run as HTML. The Angular compiler turns each template into JavaScript instructions that create and update the DOM. The question is when that happens.

    • JIT (just-in-time) compiles in the browser at runtime. The compiler (@angular/compiler) ships with the app, and templates are compiled as the app starts.
    • AOT (ahead-of-time) compiles during the build, so the browser downloads ready-to-run code.

    AOT advantages:

    • Faster startup: no compile step in the browser.
    • Smaller bundles: the compiler isn't shipped, and unused code tree-shakes better.
    • Errors at build time: broken bindings, unknown elements and, with strictTemplates, type errors in templates fail the build instead of the user's browser.
    • Security: templates aren't compiled from strings at runtime, which removes a class of template-injection risks.

    Since the Ivy compiler became standard, AOT is the default for both ng build and ng serve, so JIT is mostly a legacy concern today.

    What interviewers listen for
    • JIT compiles templates in the browser at runtime
    • AOT compiles templates during the build
    • AOT: faster startup, smaller bundles, no shipped compiler
    • AOT catches template errors at build time
    • AOT is the default for build and serve
  82. 82.How would you create a custom attribute directive, for example one that highlights an element on hover?mid

    Create a class with @Directive and an attribute selector such as [appHighlight]. Like components, directives are standalone by default, so a component adds it to imports to use it.

    • Take configuration through inputs. Giving an input the same alias as the selector lets you write appHighlight="lightblue".
    • React to events and update the host element with the host metadata, which angular.dev recommends over the classic @HostListener and @HostBinding decorators.
    • If you must touch the DOM directly, inject ElementRef and use Renderer2 rather than manipulating nativeElement yourself; it's safer with SSR.

    A structural directive is built differently: it injects TemplateRef and ViewContainerRef, and calls createEmbeddedView() or clear() to add or remove the template, for example an *appHasRole="'admin'" directive. Directives can also be composed onto components with hostDirectives.

    @Directive({
      selector: '[appHighlight]',
      host: {
        '(mouseenter)': 'hovered.set(true)',
        '(mouseleave)': 'hovered.set(false)',
        '[style.backgroundColor]': 'background()',
      },
    })
    export class Highlight {
      readonly color = input('', { alias: 'appHighlight' });
      protected readonly hovered = signal(false);
      protected readonly background = computed(() => (this.hovered() ? this.color() || 'yellow' : null));
    }
    // <p appHighlight="lightblue">Hover me</p>
    What interviewers listen for
    • @Directive with an attribute selector like [appHighlight]
    • Inputs for config; alias an input to the selector
    • host bindings and listeners instead of direct DOM access
    • Structural directives use TemplateRef + ViewContainerRef

    Likely follow-up: What are hostDirectives used for?

  83. 83.Which RxJS operators do you use most often in Angular, and what do they do?easy

    Operators are pure functions chained with pipe(). Each returns a new Observable and leaves the source untouched. The everyday set:

    • Transform: map changes each value; scan accumulates state, like reduce over time.
    • Filter: filter drops values; take(n) and first() complete after n values or the first match; distinctUntilChanged suppresses consecutive duplicates.
    • Timing: debounceTime(ms) emits only after the source has been quiet for that long; throttleTime limits the rate.
    • Side effects: tap for logging or setting a loading flag without changing the value; finalize runs on complete, error or unsubscribe.
    • Flattening: switchMap, mergeMap, concatMap, exhaustMap map values to inner Observables such as HTTP calls.
    • Combining: combineLatest, forkJoin, merge, withLatestFrom, startWith.
    • Errors: catchError to recover, retry to resubscribe.
    • Sharing: shareReplay to multicast and cache.

    The typeahead search is the classic example that uses most of them together.

    results$ = this.search.valueChanges.pipe(
      map((q) => q.trim()),
      filter((q) => q.length >= 2),
      debounceTime(300),
      distinctUntilChanged(),
      tap(() => this.loading.set(true)),
      switchMap((q) => this.api.search(q).pipe(catchError(() => of([])))),
      tap(() => this.loading.set(false)),
    );
    What interviewers listen for
    • Operators are pure functions composed with pipe()
    • map, filter, tap for transform, filter, side effects
    • debounceTime + distinctUntilChanged for user input
    • switchMap family for HTTP; catchError/retry for errors

    Likely follow-up: What is the difference between debounceTime and throttleTime?

  84. 84.What is the difference between hot and cold Observables, and why does it matter in Angular?hard

    The difference is where the producer lives.

    • A cold Observable creates its producer inside subscribe, so every subscriber gets its own, independent execution from the beginning. HttpClient requests, of, timer, interval and defer are cold.
    • A hot Observable's producer exists independently of any one subscription. Subscribers share it and only receive values emitted after they subscribe. Subjects, DOM events and a shared WebSocket stream are hot.

    Why it matters:

    • Subscribing twice to http.get(), for example two | async pipes, sends two requests.
    • A late subscriber to a hot stream misses earlier values.

    To turn a cold Observable into a shared (multicast) one, use share(), where late subscribers only get future values, or shareReplay({ bufferSize: 1, refCount: true }), which also replays the latest value to late subscribers. That's the usual way to cache an HTTP result that several components read. A BehaviorSubject or a signal in a service is the other common way to share state.

    What interviewers listen for
    • Cold: producer per subscriber; each gets its own execution
    • Hot: shared producer; late subscribers miss earlier values
    • HTTP Observables are cold: two subscribes, two requests
    • share() / shareReplay() multicast a cold source
  85. 85.What is the difference between forkJoin and combineLatest?mid

    Both combine several Observables into one, but they emit at different times.

    • forkJoin waits for every source to complete, then emits once with the last value of each (as an array or an object) and completes. It's the RxJS version of Promise.all, ideal for parallel HTTP calls. If any source errors, it errors; if any source completes without emitting, it completes without emitting anything. Don't use it with streams that never complete, such as valueChanges.
    • combineLatest emits every time any source emits, once each source has emitted at least once, with the latest value from each. It's for derived state from long-lived streams, such as data plus filters. Seed slow sources with startWith if you need an early first emission.

    Related operators: zip pairs values by index, withLatestFrom samples other streams only when the main one emits, and merge interleaves values without combining them. In signal-based code, computed() often replaces combineLatest for synchronous state.

    What interviewers listen for
    • forkJoin: one emission after all sources complete
    • forkJoin is Promise.all for Observables: parallel HTTP
    • combineLatest: emits on every change once all have emitted
    • forkJoin never emits for never-completing streams

    Likely follow-up: When would you use withLatestFrom instead of combineLatest?

  86. 86.How do you write custom synchronous and asynchronous validators for reactive forms?mid

    A synchronous validator is a ValidatorFn: a function that takes an AbstractControl and returns null when valid, or an errors object such as { forbiddenValue: true }. Wrap it in a factory function when it needs parameters. For cross-field rules like "passwords match", put the validator on the FormGroup and read the child controls.

    An async validator (AsyncValidatorFn) returns an Observable or Promise of ValidationErrors | null, typically from an HTTP call such as "is this username taken?". Details worth knowing:

    • Async validators run only after all sync validators pass.
    • While one runs, the control's status is PENDING.
    • The Observable must complete; HTTP calls do, but for other streams add first().
    • Use updateOn: 'blur', or debounce inside the validator, to avoid a request per keystroke.

    Show errors with control.hasError('taken') or control.errors?.['taken']. In template-driven forms, the same logic is wrapped in a directive that provides NG_VALIDATORS or NG_ASYNC_VALIDATORS.

    export const forbiddenValue = (bad: string): ValidatorFn => (control) =>
      control.value === bad ? { forbiddenValue: { bad } } : null;
    
    export const passwordsMatch: ValidatorFn = (group) =>
      group.get('password')?.value === group.get('confirm')?.value ? null : { mismatch: true };
    
    export const usernameAvailable = (api: UserApi): AsyncValidatorFn => (control) =>
      api.isTaken(control.value).pipe(map((taken) => (taken ? { taken: true } : null)));
    
    username = new FormControl('', {
      validators: [Validators.required, forbiddenValue('admin')],
      asyncValidators: [usernameAvailable(inject(UserApi))],
      updateOn: 'blur',
    });
    What interviewers listen for
    • ValidatorFn returns null or an errors object
    • Cross-field validators go on the FormGroup
    • Async validators return an Observable or Promise and must complete
    • Async runs only after sync validators pass; status is PENDING
    • Throttle async checks with updateOn: 'blur'
  87. 87.What are the different provider types (useClass, useValue, useFactory, useExisting), and when do you need an InjectionToken?hard

    A provider tells an injector how to produce the value for a token:

    • useClass: instantiate a class, possibly a different one than the token, e.g. { provide: Logger, useClass: ConsoleLogger }. A bare providers: [Logger] is shorthand for this.
    • useValue: supply a ready-made value, such as a config object or a test double.
    • useFactory: call a function when the token is first requested. The factory can call inject() (classic code lists its dependencies in deps), which makes it right for values computed from the environment.
    • useExisting: an alias that points one token at another, so both return the same instance. useClass here would create a second one.
    • multi: true: several providers add to one token, and injecting it returns an array, as with NG_VALIDATORS.

    An InjectionToken is needed for anything that isn't a class, because TypeScript interfaces and types disappear at runtime. new InjectionToken<AppConfig>('APP_CONFIG') gives you a typed, unique token; the string is only a debug label. A token can also carry a tree-shakable default with { providedIn: 'root', factory: () => ... }.

    export const API_URL = new InjectionToken<string>('API_URL', {
      providedIn: 'root',
      factory: () => '/api', // default when nobody provides it
    });
    
    providers: [
      { provide: Logger, useClass: environment.production ? RemoteLogger : ConsoleLogger },
      { provide: API_URL, useValue: 'https://api.example.com' },
      { provide: STORAGE, useFactory: () => (isPlatformBrowser(inject(PLATFORM_ID)) ? localStorage : memoryStorage) },
      { provide: AuditLogger, useExisting: Logger },
    ]
    
    const apiUrl = inject(API_URL); // typed as string
    What interviewers listen for
    • useClass instantiates, useValue supplies a value
    • useFactory computes the value and can call inject()
    • useExisting aliases the same instance
    • InjectionToken for non-class values like config
    • multi: true collects several providers into an array
  88. 88.Explain hierarchical injectors in Angular. How does Angular decide which instance of a service a component gets?hard

    Angular has two injector trees:

    • Element injectors follow the component tree. A component or directive with providers (or viewProviders) gets one on its host element.
    • Environment injectors hold app-wide services: the root injector (providedIn: 'root' plus the providers passed to bootstrapApplication), child injectors for routes with providers and lazy-loaded routes, then the platform injector, and finally the NullInjector, which throws.

    When a component injects a token, Angular walks up the element injectors from the requesting element, then up the environment injectors. The first provider found wins, and that injector owns the instance.

    Consequences:

    • Component providers create a new instance per component instance, shared by its descendants and destroyed with it. Good for per-widget state such as an editor or a wizard.
    • Route providers share one instance across that route's subtree.
    • viewProviders hides the service from projected content.
    • Resolution modifiers adjust the search: optional returns null instead of throwing, self looks only locally, skipSelf starts at the parent, and host stops at the host component.
    What interviewers listen for
    • Element injectors follow the component tree
    • Environment injectors: route, root, platform, then NullInjector
    • Nearest provider wins and owns the instance
    • Component providers: one instance per component
    • Modifiers: optional, self, skipSelf, host

    Likely follow-up: What is the difference between providers and viewProviders?

  89. 89.What is the inject() function, how does it differ from constructor injection, and what is an injection context?hard

    inject(Token) resolves a dependency from the current injector, exactly like a constructor parameter, but as a function call. Both go through the same injectors, so the result is the same; the differences are about where you can use it.

    inject() only works inside an injection context:

    • a constructor or a field initializer of a class Angular creates (component, directive, pipe, service),
    • a provider useFactory or an InjectionToken factory,
    • functional guards, resolvers and interceptors,
    • code wrapped in runInInjectionContext(injector, fn).

    Calling it anywhere else, such as in a click handler, ngOnInit or a setTimeout, throws NG0203. Inject in a field and use the value later.

    Why it's now preferred:

    • It works in functions, which is what made functional guards and interceptors possible.
    • Inheritance is simpler: subclasses don't have to forward dependencies to super().
    • Options are typed: inject(Logger, { optional: true }) has the type Logger | null.
    • You can build reusable helpers that call inject() internally.
    What interviewers listen for
    • Same resolution as constructor injection, as a function
    • Only valid in an injection context, else NG0203
    • Enables functional guards, resolvers, interceptors
    • Simplifies inheritance; typed options object
    • runInInjectionContext for code outside those contexts
  90. 90.What is ChangeDetectorRef, and what is the difference between markForCheck() and detectChanges()?hard

    ChangeDetectorRef gives a component manual control over its own change detection. Its main methods:

    • markForCheck() marks this view and all its ancestors dirty, so they're checked in the next pass. It doesn't run a check itself; in a zoneless app it also schedules that next pass. It's the fix when an OnPush component assigns a plain field in a callback Angular doesn't know about. The async pipe calls it for you.
    • detectChanges() checks this view and its children synchronously, right now. Use it with detach(), or in tests. Reaching for it to silence NG0100 errors is usually a sign of broken data flow.
    • detach() removes the view from the tree so passes skip it; reattach() puts it back. That suits widgets that refresh on their own schedule, like a high-frequency ticker.
    • checkNoChanges() is the dev-mode verification check.

    In modern code you rarely need any of these: signals, the async pipe and toSignal() notify Angular automatically. Turning that plain field into a signal removes the need for markForCheck().

    What interviewers listen for
    • markForCheck(): marks view and ancestors for the next pass
    • detectChanges(): checks this subtree synchronously now
    • detach()/reattach() take a view out of normal checking
    • Signals and the async pipe make manual calls rare
  91. 91.How does two-way binding work under the hood, and how do you make your own component support [( )] syntax?mid

    The "banana in a box" syntax is pure sugar. [(size)]="fontSize" compiles to a property binding plus an event binding:

    [size]="fontSize" (sizeChange)="fontSize = $event"

    So any component with an input x and an output named xChange supports two-way binding. The data flow is still one-way in each direction: the value goes down through the input, and changes come back up through the event.

    • Modern: model() declares both at once. size = model(16) is a writable signal; calling this.size.set(20) updates it locally and emits sizeChange. The parent can bind a plain field or a writable signal, passing the signal itself without calling it: [(size)]="fontSize".
    • Classic: @Input() size plus @Output() sizeChange = new EventEmitter<number>(), emitting manually.

    [(ngModel)] follows the same pattern: [ngModel] plus (ngModelChange). Splitting it is handy when you want to transform the value, e.g. (ngModelChange)="name = $event.trim()".

    What interviewers listen for
    • [(x)] is [x] plus (xChange)
    • Any input x with an output xChange supports it
    • model() declares the input and Change output together
    • Parent can bind a writable signal directly
  92. 92.What is a route resolver, and what are the trade-offs of using one?mid

    A resolver loads data before a route activates, so the component renders with its data already available. In modern Angular it's a function of type ResolveFn<T> that can call inject() and returns a value, a Promise or an Observable. You register it under the route's resolve key.

    How it behaves:

    • Resolvers run after all guards have passed.
    • The router takes the first value an Observable emits. If it completes without emitting, the navigation is cancelled.
    • If it errors, the navigation fails, so catch errors and return a fallback or a RedirectCommand to a "not found" page.
    • The component reads the result from ActivatedRoute.data, or directly as an input with withComponentInputBinding().

    The trade-off is perceived performance: nothing on screen changes until the data arrives, so the app can feel frozen on slow connections. Show a global progress bar from router events, or load the data in the component instead (with httpResource() or an Observable) and render a skeleton. Resolvers fit best when the page is meaningless without its data.

    What interviewers listen for
    • Loads data before the route activates
    • Functional ResolveFn with inject(); runs after guards
    • Uses the first emitted value; no value cancels navigation
    • Read via ActivatedRoute.data or component inputs
    • Trade-off: navigation waits, so the UI can feel stuck
  93. 93.What are @defer blocks, and how do they work?mid

    @defer lazy loads a section of a template. The components, directives and pipes used only inside the block are split into a separate chunk and loaded when a trigger fires, which keeps them out of the initial bundle.

    Triggers:

    • on idle (the default): when the browser is idle.
    • on viewport, on interaction, on hover: based on the placeholder or a referenced element.
    • on immediate, on timer(2s), or when condition for a custom expression.
    • prefetch on ... downloads the code earlier than it's rendered.

    Companion blocks: @placeholder shows before loading starts (with an optional minimum display time), @loading shows while the chunk downloads (after and minimum prevent flicker), and @error shows if loading fails.

    Gotchas: dependencies must be standalone, and if they're referenced anywhere else in the same file (another part of the template, or a viewChild query) they're bundled eagerly. During SSR, the server renders the @placeholder. With incremental hydration, hydrate on triggers let the server render the real content and hydrate it later.

    @defer (on viewport; prefetch on idle) {
      <app-comments [postId]="postId()" />
    } @placeholder (minimum 300ms) {
      <p>Comments</p>
    } @loading (after 100ms; minimum 500ms) {
      <app-spinner />
    } @error {
      <p>Could not load comments.</p>
    }
    What interviewers listen for
    • Lazy loads template dependencies into a separate chunk
    • Triggers: idle (default), viewport, interaction, hover, timer, when
    • @placeholder, @loading, @error sub-blocks
    • Dependencies must be standalone and not used elsewhere eagerly
    • SSR renders the placeholder unless using incremental hydration

    Likely follow-up: When would you pick @defer over lazy loading a route?

  94. 94.What is view encapsulation in Angular, and what are the available modes?mid

    View encapsulation controls whether a component's styles stay inside that component. It's set with the encapsulation option:

    • ViewEncapsulation.Emulated (the default): Angular adds generated attributes to the component's elements (like _ngcontent-... and _nghost-... on the host) and rewrites its CSS selectors to include them. Styles don't leak out to other components, but global styles still apply inside.
    • ViewEncapsulation.ShadowDom: uses the browser's native Shadow DOM, so isolation works in both directions. Only inherited properties and CSS custom properties cross the boundary.
    • ViewEncapsulation.None: no scoping; the styles become global once the component loads.

    Related selectors: :host styles the component's own host element, and :host-context(.dark) applies styles based on an ancestor. ::ng-deep pushes a rule into child components; it's deprecated but still supported. For theming children, CSS custom properties are the cleaner option, since they pass through every encapsulation mode.

    What interviewers listen for
    • Emulated (default): attribute-scoped CSS, styles do not leak out
    • ShadowDom: native isolation in both directions
    • None: component styles become global
    • :host styles the host; ::ng-deep is deprecated
    • CSS custom properties theme across boundaries
  95. 95.What are template reference variables, and what can they refer to?easy

    A template reference variable is declared with #name on an element, and gives you a handle you can use elsewhere in the same template. What it refers to depends on where it's placed:

    • On a plain element: the DOM element, e.g. an HTMLInputElement.
    • On a component: the component instance, so the template can call its public methods.
    • With a directive's exportAs name: that directive instance, e.g. #f="ngForm" or #m="matMenu".
    • On an <ng-template>: its TemplateRef.

    Common uses are reading an input's value without binding it to a field, letting one element control another, and naming elements for viewChild('name').

    Scope: a variable is visible throughout its template, but one declared inside an @if or @for block (or an <ng-template>) isn't visible outside that block. For values computed in the template, @let declares a local variable instead.

    <input #box placeholder="New todo" (keyup.enter)="add(box.value)" />
    <button (click)="add(box.value)">Add</button>
    
    <app-video-player #player src="intro.mp4" />
    <button (click)="player.play()">Play</button>
    
    <form #f="ngForm" (ngSubmit)="save(f.value)">
      <input name="email" ngModel required />
      <button [disabled]="f.invalid">Save</button>
    </form>
    What interviewers listen for
    • Declared with #name in the template
    • Element: DOM node; component: its instance
    • #f="ngForm" binds a directive via exportAs
    • On ng-template it is a TemplateRef
    • Not visible outside the block it was declared in
  96. 96.How would you optimize the performance of a slow Angular application?hard

    First measure: the Angular DevTools profiler shows which components are checked and how long each pass takes, Lighthouse covers load metrics, and bundle analysis shows what's in each chunk. Then work on two fronts.

    Load time

    • Lazy load routes with loadComponent/loadChildren, and heavy template sections with @defer.
    • Enforce bundle budgets in angular.json, and keep large libraries out of the main chunk.
    • Use SSR or prerendering with hydration for a faster first paint.
    • Use NgOptimizedImage (ngSrc, plus priority on the LCP image) for sizing and lazy loading of images.

    Runtime

    • OnPush and signals, so change detection only touches affected views. Zoneless removes Zone.js overhead.
    • A stable track expression in @for (trackBy for *ngFor), so DOM nodes are reused instead of recreated.
    • Pure pipes or computed() instead of method calls in templates, which re-run on every check.
    • Virtual scrolling (CDK cdk-virtual-scroll-viewport) for long lists.
    • Debounce noisy input, cancel stale requests with switchMap, and clean up subscriptions.
    • In Zone.js apps, run high-frequency work outside Angular with NgZone.runOutsideAngular().
    What interviewers listen for
    • Measure first: DevTools profiler, Lighthouse, bundle analysis
    • Lazy load routes and @defer heavy sections
    • OnPush, signals, zoneless to reduce change detection work
    • track in @for; no method calls in templates
    • Virtual scrolling, NgOptimizedImage, SSR

    Likely follow-up: What does the track expression in @for actually do?

  97. 97.How do you handle errors in an Angular application, both for HTTP calls and globally?mid

    Errors are handled at several levels:

    • Per request: HttpClient errors with an HttpErrorResponse. status holds the HTTP status code, and 0 usually means a network or CORS failure. Use catchError to return a fallback (of([])) or rethrow a friendlier error with throwError(() => ...). Inside a switchMap, catch on the inner Observable so the outer stream survives.
    • Retries: retry({ count: 2, delay: 1000 }) resubscribes, which re-sends the request. Use it for transient failures and idempotent requests, not blindly for writes.
    • Cross-cutting: an interceptor centralizes behavior like redirecting to login on 401 or showing a toast.
    • Global: uncaught errors are sent to ErrorHandler, which logs to the console by default. Provide your own implementation to report to a monitoring service. provideBrowserGlobalErrorListeners() also forwards the window's error and unhandledrejection events to it.

    With signals, toSignal() rethrows the Observable's error when the signal is read, and resource() exposes an error() signal and an 'error' status for the template to render.

    What interviewers listen for
    • catchError with a fallback or a rethrow; catch inside inner streams
    • retry with count and delay for transient failures
    • Interceptors centralize cross-cutting error handling
    • Custom ErrorHandler for global reporting
  98. 98.When would you use signals and when would you use RxJS Observables in an Angular app?mid

    They solve different problems, and they interoperate.

    Signals model state: a value that always exists and changes over time. Reads are synchronous, computed() derivations are memoized and glitch-free, there's no subscription to manage, and templates that read them get fine-grained change detection. Use them for component state, derived values, inputs and shared state in services.

    Observables model events and async streams: things that happen over time, possibly with no current value. They shine at time and concurrency logic: debouncing, cancellation with switchMap, retries, combining several async sources, WebSockets. HttpClient, the router and reactive forms all expose Observables.

    A common split is RxJS at the edges for async orchestration and signals for the state the template reads. Bridge the two with toSignal() and toObservable(), and use resource() or httpResource() for signal-driven data fetching.

    Signals don't replace all of RxJS: a signal has no notion of completion or error events, and if you set it twice synchronously, consumers only see the last value.

    What interviewers listen for
    • Signals: synchronous state and derived values
    • Observables: events, async streams, time and concurrency
    • Signals need no subscriptions and give fine-grained updates
    • Bridge with toSignal() / toObservable()

    Likely follow-up: How does toSignal() handle errors from the Observable?

  99. 99.What role does Zone.js play in Angular, and what changes with zoneless change detection?hard

    Zone.js monkey-patches the browser's async APIs (addEventListener, setTimeout, Promise, XHR and more), so Angular hears about every task that runs. When a task finishes, NgZone triggers ApplicationRef.tick(), a change detection pass from the root. That's why mutating any field in any callback "just works".

    The costs: extra bundle size and startup work, a pass after every task even if nothing changed (a mousemove handler, a polling timer), noisier stack traces, and native async/await can't be patched, so the CLI has to downlevel it.

    Zoneless change detection drops Zone.js. Angular schedules a pass only when it's notified: a signal read in a template changes, a template or host listener runs, markForCheck() is called (the async pipe does this), ComponentRef.setInput() runs, or a view is attached or removed. Recent Angular versions are zoneless by default; older apps opt in with provideZonelessChangeDetection() and remove zone.js from the polyfills.

    Migration checklist: keep async state in signals or the async pipe, make components OnPush-compatible, replace NgZone.onStable with afterNextRender(), and use PendingTasks so SSR waits for async work.

    What interviewers listen for
    • Zone.js patches async APIs and triggers tick()
    • Costs: bundle size, unnecessary passes, async/await downleveling
    • Zoneless: signals, listeners, markForCheck, setInput notify
    • Plain field changes in callbacks no longer render
    • provideZonelessChangeDetection(); default in recent versions
  100. 100.How do toSignal() and toObservable() work, and what should you watch out for?hard

    Both come from @angular/core/rxjs-interop and bridge the two reactive models.

    toSignal(obs$):

    • subscribes immediately (not lazily on first read) and exposes the latest value as a read-only signal;
    • unsubscribes automatically when its injection context (component, directive, service) is destroyed, so it must be called in an injection context or be given an injector;
    • is undefined until the first emission, unless you pass initialValue, or requireSync: true for sources that emit synchronously such as a BehaviorSubject;
    • rethrows the Observable's error whenever the signal is read, so handle errors in the pipe.

    toObservable(sig):

    • pushes values through an effect, so emissions are asynchronous, and several synchronous set() calls collapse into a single emission of the latest value;
    • also needs an injection context.

    The classic pattern is signal, then toObservable, then RxJS operators such as debounceTime and switchMap, then toSignal for the template. For "refetch when a signal changes", rxResource() or httpResource() express it more directly.

    export class UserSearch {
      private readonly api = inject(UserApi);
      readonly query = signal('');
    
      readonly results = toSignal(
        toObservable(this.query).pipe(
          debounceTime(300),
          distinctUntilChanged(),
          switchMap((q) => this.api.search(q).pipe(catchError(() => of([])))),
        ),
        { initialValue: [] },
      );
    }
    What interviewers listen for
    • toSignal subscribes immediately and cleans up on destroy
    • undefined until first value unless initialValue/requireSync
    • toSignal rethrows errors on read
    • toObservable emits asynchronously via an effect
    • Both need an injection context
  101. 101.How do typed reactive forms work? Why is a FormControl often typed as string | null?mid

    FormControl, FormGroup and FormArray carry the type of their value, inferred from the initial values, so form.value, valueChanges and setValue() are all type-checked.

    The details interviewers probe:

    • new FormControl('') is FormControl<string | null>, because reset() sets a control to null by default. Pass { nonNullable: true }, or build the form with NonNullableFormBuilder (fb.nonNullable), and reset() returns to the initial value, so the type is just string.
    • form.value is a Partial<...> because disabled controls are left out of it. form.getRawValue() includes every control and has the full type.
    • form.controls.email is fully typed, so a misspelled key is a compile error.
    • FormRecord handles dynamic keys with a single value type, and FormArray<FormControl<string>> a typed list.
    • UntypedFormControl and UntypedFormGroup are escape hatches for migrating older code.
    export class Signup {
      private readonly fb = inject(NonNullableFormBuilder);
    
      readonly form = this.fb.group({
        email: ['', [Validators.required, Validators.email]],
        age: [18],
      });
      // form.controls.email: FormControl<string>
      // form.value:          Partial<{ email: string; age: number }>
      // form.getRawValue():  { email: string; age: number }
      clear() {
        this.form.reset(); // back to '' and 18, never null
      }
    }
    What interviewers listen for
    • Controls and groups carry their value type
    • reset() sets null by default, hence string | null
    • nonNullable / NonNullableFormBuilder remove null
    • value is Partial (disabled omitted); getRawValue() is complete
    • FormRecord for dynamic keys
  102. 102.What is ControlValueAccessor, and how do you make a custom component work with formControlName and ngModel?hard

    ControlValueAccessor is the bridge between Angular forms and a form control's UI. Built-in accessors connect <input>, <select> and checkboxes. For a custom widget, such as a star rating or a date picker, you implement it yourself:

    • writeValue(value): the form pushes a value into the component (initial value, setValue, reset).
    • registerOnChange(fn): save fn and call it when the user changes the value, to update the form model.
    • registerOnTouched(fn): save it and call it on blur or interaction, so touched and updateOn: 'blur' work.
    • setDisabledState(isDisabled) (optional): react to control.disable().

    Register the component as an accessor with an NG_VALUE_ACCESSOR provider, using multi: true and useExisting. forwardRef is needed because the class is referenced inside its own decorator, before it's defined.

    After that, the component works with formControlName, [formControl] and [(ngModel)], and validators, touched state and CSS status classes all come for free. To bundle a built-in validation rule with the component, also provide NG_VALIDATORS and implement Validator.

    @Component({
      selector: 'app-star-rating', templateUrl: './star-rating.html',
      providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StarRating), multi: true }],
    })
    export class StarRating implements ControlValueAccessor {
      protected readonly value = signal(0);
      protected readonly disabled = signal(false);
      private onChange = (_: number) => {}; private onTouched = () => {};
      writeValue(v: number | null) { this.value.set(v ?? 0); }
      registerOnChange(fn: (v: number) => void) { this.onChange = fn; }
      registerOnTouched(fn: () => void) { this.onTouched = fn; }
      setDisabledState(d: boolean) { this.disabled.set(d); }
      protected select(n: number) { this.value.set(n); this.onChange(n); this.onTouched(); }
    }
    What interviewers listen for
    • Bridges the forms API and a custom UI control
    • writeValue, registerOnChange, registerOnTouched, setDisabledState
    • Provide NG_VALUE_ACCESSOR with useExisting and multi: true
    • forwardRef because the class is not yet defined
    • Then works with formControlName and ngModel
  103. 103.How do you manage state in an Angular application? When would you use NgRx instead of services?hard

    I pick the lightest tool that fits:

    • Local state: signals inside the component.
    • Shared state: a service with signals. Keep a private writable signal, expose read-only state through asReadonly() and computed() selectors, and make methods the only way to change it. Scope it with providedIn: 'root' or component and route providers. Before signals, the same pattern used a private BehaviorSubject.
    • NgRx Store: the Redux pattern. Actions describe events, pure reducers produce new state, selectors derive memoized slices, and effects handle side effects such as HTTP. You get a single source of truth, one-way data flow, Redux DevTools with time travel and shared conventions, at the cost of boilerplate and indirection.
    • NgRx SignalStore (@ngrx/signals): a lighter, signal-based store built from features like withState, withComputed and withMethods.

    I start with services and signals, and move to NgRx when many features share complex state, changes need to be traceable, or a large team benefits from enforced conventions. Server data (caching, refetching) often fits httpResource() or a query library better than a global store.

    @Injectable({ providedIn: 'root' })
    export class TodoStore {
      private readonly todosState = signal<Todo[]>([]);
    
      readonly todos = this.todosState.asReadonly();
      readonly remaining = computed(() => this.todos().filter((t) => !t.done).length);
    
      add(title: string) {
        this.todosState.update((list) => [...list, { id: crypto.randomUUID(), title, done: false }]);
      }
      toggle(id: string) {
        this.todosState.update((list) => list.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
      }
    }
    What interviewers listen for
    • Local state in component signals
    • Shared state: service with private signal, public computed()
    • NgRx: actions, reducers, selectors, effects; DevTools
    • NgRx trades boilerplate for predictability at scale
    • Start simple; adopt a store when complexity demands it
  104. 104.What are server-side rendering and hydration in Angular, and what do you need to watch out for?hard

    SSR renders the app to HTML on the server (Node) for each request, so users see content before the JavaScript loads, which helps first paint, LCP and SEO. Prerendering (SSG) does the same at build time. You add it with ng new --ssr or ng add @angular/ssr, and a server routes config picks a render mode per route.

    Hydration (provideClientHydration()) lets the browser reuse the server-rendered DOM instead of re-creating it, avoiding flicker and wasted work. Useful features:

    • withEventReplay() records clicks made before hydration finishes and replays them.
    • withIncrementalHydration() with @defer (hydrate on viewport) keeps parts of the page dehydrated until needed.
    • HttpClient's transfer cache reuses GET responses fetched on the server, so the client doesn't request them again.

    Watch out for:

    • Browser-only APIs (window, localStorage) don't exist on the server. Use afterNextRender() or isPlatformBrowser().
    • Direct DOM manipulation causes hydration mismatches. Use Angular APIs, or opt out with ngSkipHydration.
    • The server waits for the app to become stable, so pending async work delays the response.
    What interviewers listen for
    • SSR renders HTML on the server; SSG prerenders at build time
    • Hydration reuses server DOM instead of re-rendering
    • Event replay and incremental hydration via @defer (hydrate on ...)
    • Browser APIs go in afterNextRender() or behind isPlatformBrowser
    • Direct DOM manipulation breaks hydration
  105. 105.How does Angular protect against XSS, and when would you use DomSanitizer?mid

    Angular treats every bound value as untrusted by default:

    • Interpolation {{ }} and text bindings are escaped, so <script> shows up as text.
    • [innerHTML] is sanitized: scripts, event-handler attributes and other dangerous markup are stripped, with a dev-mode warning.
    • URL bindings such as [href] and [src] are sanitized, so a javascript: URL is neutralized.
    • Resource URLs, such as an <iframe> src, can't be bound to untrusted values at all; Angular throws.

    DomSanitizer has bypassSecurityTrustHtml, ...Url, ...ResourceUrl, ...Style and ...Script, which mark a value as trusted and skip sanitization. Use them only for content you fully control or have sanitized yourself, such as an embed URL you built from a video id, never for raw user input.

    You lose this protection by going around Angular: writing to nativeElement.innerHTML, building templates from user input, or misusing the bypass methods. Add a CSP and Trusted Types for defense in depth. For CSRF, HttpClient reads an XSRF-TOKEN cookie and sends it as an X-XSRF-TOKEN header on mutating requests to relative URLs.

    export class VideoEmbed {
      private readonly sanitizer = inject(DomSanitizer);
      readonly videoId = input.required<string>();
    
      // We build the URL ourselves from a validated id, so trusting it is safe
      readonly src = computed(() =>
        this.sanitizer.bypassSecurityTrustResourceUrl(
          `https://www.youtube.com/embed/${encodeURIComponent(this.videoId())}`,
        ),
      );
    }
    // <iframe [src]="src()"></iframe>
    What interviewers listen for
    • Bound values are untrusted and escaped or sanitized by default
    • [innerHTML] is sanitized; resource URLs need explicit trust
    • DomSanitizer.bypassSecurityTrust* only for controlled content
    • Direct DOM writes bypass protection
    • CSP, Trusted Types, and XSRF support in HttpClient
  106. 106.How do you unit test Angular components and services? What are TestBed and component harnesses?mid

    TestBed creates a small Angular environment for a test. You configure it like an app, with imports (the standalone component under test) and providers (fakes for its dependencies), then:

    • TestBed.createComponent(Comp) returns a ComponentFixture, with componentInstance, nativeElement and componentRef.setInput() for inputs.
    • Trigger rendering with fixture.detectChanges(), or in zoneless tests await fixture.whenStable().
    • Services are tested with TestBed.inject(Service), or even as plain classes.
    • For HTTP, add provideHttpClient() and provideHttpClientTesting(), then use HttpTestingController to expectOne() a request, flush() a response and verify() that nothing else was sent.

    Component harnesses (from @angular/cdk/testing) are page-object APIs for components, e.g. MatButtonHarness. Tests call await button.click() instead of querying DOM internals, so they keep working when a component's markup changes. Angular Material ships harnesses for its components, and you can write your own.

    New CLI projects use Vitest as the default runner; older projects typically run Karma with Jasmine. End-to-end tests usually use Playwright or Cypress.

    it('shows the user name', async () => {
      TestBed.configureTestingModule({
        imports: [UserCard],
        providers: [{ provide: UserService, useValue: { current: signal({ name: 'Ada' }) } }],
      });
    
      const fixture = TestBed.createComponent(UserCard);
      fixture.componentRef.setInput('compact', true);
      await fixture.whenStable();
    
      expect(fixture.nativeElement.textContent).toContain('Ada');
    });
    What interviewers listen for
    • TestBed configures imports and fake providers
    • createComponent returns a fixture; setInput for inputs
    • detectChanges() or whenStable() to render
    • HttpTestingController to expect and flush requests
    • Harnesses test through a stable API, not DOM details
  107. 107.What is the Angular CLI, and which commands do you use most?easy

    The Angular CLI (ng) scaffolds, builds, serves, tests and updates Angular projects, and applies code changes through schematics.

    • ng new my-app: create a workspace with a configured project.
    • ng serve: start the dev server with rebuilds on save.
    • ng generate (ng g): scaffold a component, service, directive, pipe, guard, interceptor and more, e.g. ng g c user-card.
    • ng build: a production build by default, with AOT compilation, minification, tree-shaking, hashed file names and bundle budget checks.
    • ng test: run unit tests.
    • ng add <package>: install a library and run its setup schematic, e.g. ng add @angular/material or ng add @angular/ssr.
    • ng update: upgrade Angular and run migrations that rewrite your code for breaking changes.

    Project configuration lives in angular.json: builders, build configurations (development, production), fileReplacements for environments, budgets and assets. New projects use the esbuild-based application builder with a Vite-powered dev server.

    What interviewers listen for
    • ng new, ng serve, ng generate, ng build, ng test
    • ng build is a production build: AOT, minified, tree-shaken
    • ng add installs and configures libraries
    • ng update upgrades and runs migrations
    • angular.json holds builders, configurations and budgets
  108. 108.What is linkedSignal, and how is it different from computed?hard

    linkedSignal creates a writable signal whose value is derived from other signals but can also be overridden locally.

    • Like computed(), it recomputes whenever the signals it depends on change.
    • Unlike computed(), you can call set() and update() on it. A local value holds until the source changes again, and then the computation resets it.

    Typical cases are a selection that should reset when its list of options changes, and an editable draft of an input value that resets when a new record arrives.

    There are two forms:

    • Shorthand: linkedSignal(() => this.options()[0]).
    • With { source, computation }: the computation receives the new source value and previous ({ source, value }), so it can keep the user's choice when it's still valid.

    It replaces the anti-pattern of an effect() that copies one signal into another writable signal. If the value never needs a manual override, use computed().

    export class ShippingPicker {
      readonly options = input.required<ShippingOption[]>();
    
      // Resets to the first option whenever options change; the user can still pick another
      readonly selected = linkedSignal(() => this.options()[0]);
    
      // Keeps the user's choice if it still exists in the new list
      readonly kept = linkedSignal<ShippingOption[], ShippingOption>({
        source: this.options,
        computation: (opts, previous) => opts.find((o) => o.id === previous?.value.id) ?? opts[0],
      });
    }
    What interviewers listen for
    • Writable signal derived from a source
    • Local set() holds until the source changes
    • computed() is read-only; linkedSignal is overridable
    • previous lets you preserve a still-valid value
    • Replaces effects that copy one signal into another
  109. 109.What are resource() and httpResource(), and what problem do they solve?hard

    They bring async data loading into the signal model, so you don't have to hand-wire "when this signal changes, fetch, track loading, handle errors".

    resource() takes a params function that reads signals and a loader that returns a Promise. Whenever the params change, it runs the loader again and aborts the previous run through the abortSignal it passes in. If params returns undefined, the loader doesn't run and the resource stays idle. It exposes signals and methods:

    • value(), hasValue(), error(), isLoading(),
    • status(): 'idle', 'loading', 'reloading', 'resolved', 'error' or 'local' (after a local set()),
    • reload(), set() and update().

    Variants: rxResource() from @angular/core/rxjs-interop takes an Observable-returning stream function, and httpResource() from @angular/common/http takes a reactive URL or request and goes through HttpClient, so interceptors and testing tools apply.

    Pitfalls: value() throws while the resource is in the error state, so check hasValue() or error() in the template. Resources are for reading data; send writes (POST, PUT, DELETE) with HttpClient directly.

    export class UserDetail {
      readonly id = input.required<string>();
    
      readonly user = httpResource<User>(() => `/api/users/${this.id()}`);
    }
    
    // Template:
    // @if (user.isLoading()) { <app-spinner /> }
    // @else if (user.error()) { <p>Could not load user.</p> }
    // @else if (user.hasValue()) { <h2>{{ user.value().name }}</h2> }
    What interviewers listen for
    • Signal-driven async loading with status, value and error signals
    • Re-runs when params change and aborts the previous load
    • rxResource for Observables, httpResource via HttpClient
    • value() throws in error state: check hasValue()
    • Meant for reads, not mutations
  110. 110.What causes the ExpressionChangedAfterItHasBeenCheckedError (NG0100), and how do you fix it properly?hard

    It's a development-mode check. After each change detection pass, Angular runs a second verification pass over the same bindings. If one now produces a different value than the one it just rendered, it throws NG0100, because the screen would disagree with the state. Production builds skip the check, so there the same bug silently shows stale values.

    Common causes:

    • Changing a bound value in ngAfterViewInit or ngAfterViewChecked, after the view was already checked.
    • A child updating its parent's state (through a shared service or a synchronous output) while the parent is being checked.
    • Template methods or getters that return a different primitive on every call, like Date.now() or Math.random().

    Proper fixes:

    • Set initial values earlier, in the constructor or ngOnInit.
    • Derive values with computed() instead of assigning them in lifecycle hooks, or make the value a signal.
    • Keep template expressions pure and stable.
    • Restructure data flow so the parent owns the state.

    Wrapping the assignment in setTimeout or calling detectChanges() hides the symptom without fixing the flow.

    @Component({
      selector: 'app-banner',
      changeDetection: ChangeDetectionStrategy.Eager, // named Default in older versions
      template: `<p>{{ message }}</p>`,
    })
    export class Banner implements AfterViewInit {
      message = 'Loading';
    
      ngAfterViewInit() {
        this.message = 'Loaded'; // NG0100: the view was already checked with 'Loading'
      }
    }
    What interviewers listen for
    • Dev-mode verification pass found a binding that changed
    • Classic cause: changing state in ngAfterViewInit
    • Also: child mutating parent state, impure template expressions
    • Fix the data flow: earlier init, computed(), signals
    • setTimeout or detectChanges() only hide it
  111. 111.How do you render a component dynamically when you only know which one at runtime?hard

    Common cases are dialogs, dashboards built from configuration and plugin widgets. There are three main tools:

    • In a template: NgComponentOutlet, e.g. <ng-container *ngComponentOutlet="widget.component; inputs: widget.inputs" />. It's declarative, and the simplest option when a config object decides the component.
    • In code: ViewContainerRef.createComponent(Comp) inserts the component at a location, taken from inject(ViewContainerRef) or a query with { read: ViewContainerRef }. It returns a ComponentRef. Set inputs with ref.setInput('title', value), which also marks it for check, subscribe to outputs through ref.instance, and call ref.destroy() when you're done. Newer versions also accept bindings such as inputBinding() and outputBinding() at creation time.
    • Outside any view, for example a toast attached to document.body: the standalone createComponent(Comp, { environmentInjector, hostElement }), then appRef.attachView(ref.hostView) so change detection reaches it.

    Combine these with a dynamic import() so the component's code loads only when needed. Older code went through ComponentFactoryResolver; since Ivy you pass the component class directly, and recent versions have removed that API.

    @Component({
      selector: 'app-dashboard',
      template: `<ng-container #slot />`,
    })
    export class Dashboard {
      private readonly slot = viewChild.required('slot', { read: ViewContainerRef });
    
      async addChart(title: string) {
        const { SalesChart } = await import('./sales-chart');
        const ref = this.slot().createComponent(SalesChart);
        ref.setInput('title', title);
        ref.instance.closed.subscribe(() => ref.destroy());
      }
    }
    What interviewers listen for
    • NgComponentOutlet for declarative dynamic rendering
    • ViewContainerRef.createComponent() returns a ComponentRef
    • setInput() for inputs; destroy() to clean up
    • Pair with dynamic import() for lazy loading
    • No ComponentFactoryResolver needed any more
  112. 112.How does internationalization (i18n) work in Angular?mid

    Angular's built-in i18n uses @angular/localize (added with ng add @angular/localize) and works at build time:

    • Mark template text with the i18n attribute, optionally with a meaning, a description and a custom id: i18n="site header|Welcome message@@homeWelcome". Attributes use i18n-<attr>, e.g. i18n-title.
    • Mark strings in TypeScript with the $localize tagged template.
    • Handle plurals and alternatives with ICU expressions such as {count, plural, =0 {...} other {...}} and select.
    • Run ng extract-i18n to generate a source translation file (XLIFF by default); translators return one file per locale.
    • Configure locales in angular.json and build with localize. The CLI produces a separate, fully translated build per locale, so there's no runtime lookup cost.

    Locale-aware pipes (date, currency, number) follow LOCALE_ID.

    The trade-off: switching language means loading a different build, usually served under a URL prefix like /fr/. For switching at runtime without a reload, teams use libraries such as Transloco or ngx-translate.

    <h1 i18n="site header|Welcome message@@homeWelcome">Welcome to Stacktrick</h1>
    
    <img [src]="logoUrl" i18n-alt alt="Stacktrick logo" />
    
    <span i18n>
      {count, plural, =0 {No new messages} =1 {One new message} other {{{count}} new messages}}
    </span>
    What interviewers listen for
    • Mark text with i18n, attributes with i18n-attr
    • $localize for strings in TypeScript
    • ICU expressions for plural and select
    • ng extract-i18n produces translation files
    • One compiled build per locale; runtime switching needs a library

Prefer multiple choice? All 32 Angular MCQs with answers →

esc