Top 5 Angular Interview Mistakes in 2026

·11 min read
By ·Updated
angularinterview-questionsinterview-tipsfrontendcareer

Angular interview answers age quickly when they are memorized as slogans. As of September 2026, Angular 22 is active, Angular 21 is in LTS, zoneless change detection is the default from Angular 21, and OnPush is the default change-detection strategy from Angular 22.

These five mistakes are less about missing an API name and more about using the wrong model. A strong answer names the Angular version, identifies the boundary or lifecycle involved, and explains how you would verify the behavior.

Table of Contents

  1. Using a pre-zoneless change-detection model
  2. Treating every RxJS subscription the same
  3. Calling every service a singleton
  4. Reciting lifecycle slogans
  5. Optimizing from a checklist instead of evidence

Mistake 1: Using a Pre-Zoneless Change-Detection Model

Why is “OnPush only checks reference equality” an incomplete answer?

That sentence mixes an input-comparison rule with the full scheduling model. An OnPush subtree can become eligible for checking through several notifications, including:

  • a bound input update;
  • a template, output, or host listener in the subtree;
  • a signal read by the template changing;
  • AsyncPipe receiving a value;
  • ComponentRef.setInput;
  • ChangeDetectorRef.markForCheck().

Angular's current subtree guide says bound input values are compared with ==. Writable signals use their configured equality function and use referential Object.is() by default. Neither rule means “Angular deep-compares my object.”

Consider a child that receives an array:

@Component({
  selector: 'user-list',
  template: `
    @for (user of users(); track user.id) {
      <p>{{ user.name }}</p>
    }
  `,
})
export class UserList {
  users = input.required<readonly User[]>();
}

Mutating the parent's existing array does not create a new input value:

// Ambiguous boundary: same array reference.
this.users.push(newUser);
 
// Explicit boundary update: new array reference.
this.users = [...this.users, newUser];

The first form can appear to work if some other notification causes a check, which is why “it updated on my machine” does not disprove the boundary problem. Explain which notification causes the view to be checked.

Angular 21+ is zoneless by default. A setTimeout callback is not, by itself, a general change-detection notification in a zoneless application. Updating a template-read signal is:

readonly status = signal('idle');
 
load() {
  setTimeout(() => this.status.set('ready'), 100);
}

A strong interview answer distinguishes input equality from signal equality, scheduling a future check with markForCheck() from synchronously checking a view with detectChanges(), and notification-based zoneless behavior from the older “Zone.js notices every async task” model.

Read the deeper Angular change detection interview guide after you can explain that model without a four-trigger mnemonic.


Mistake 2: Treating Every RxJS Subscription the Same

Why is “always unsubscribe in ngOnDestroy” not enough?

Subscription ownership depends on lifetime. A normal HttpClient request generally emits and completes; a router event stream, timer, DOM event, subject, or store selector can outlive a component. Unsubscribing also cancels local observation, but it does not universally prove that a remote system reversed work already accepted.

Prefer a template-owned subscription when the value is only rendered:

@Component({
  selector: 'dashboard',
  template: `
    @if (dashboard$ | async; as dashboard) {
      <dashboard-view [data]="dashboard" />
    }
  `,
  imports: [AsyncPipe, DashboardView],
})
export class Dashboard {
  private api = inject(DashboardApi);
  readonly dashboard$ = this.api.load();
}

For an imperative, long-lived subscription, Angular provides takeUntilDestroyed:

import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
 
export class Notifications {
  private destroyRef = inject(DestroyRef);
  private feed = inject(NotificationFeed);
 
  start() {
    this.feed.messages
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(message => this.show(message));
  }
}

When takeUntilDestroyed() is called outside an injection context, pass a DestroyRef explicitly. This is clearer than maintaining a hand-written Subject<void> solely for teardown.

Operator questions also need semantics, not slogans:

OperatorConcurrent inner workOrderingTypical risk
switchMapOne active subscriptionLatest winsPrior result is no longer wanted
concatMapOne at a timePreservedQueue can grow
mergeMapConfigurable concurrencyCompletion orderConcurrent side effects
exhaustMapIgnores new values while activeFirst wins during windowDropped repeated triggers

switchMap unsubscribes from the previous inner Observable. Whether that aborts an HTTP transport or cancels server-side work depends on the client and server contract. For write operations, discuss idempotency and reconciliation rather than assuming operator choice provides distributed cancellation.

See the Angular RxJS interview guide for a broader operator review.


Mistake 3: Calling Every Service a Singleton

Why does provider placement matter?

Angular resolves tokens through hierarchical injectors. “Singleton” is only meaningful relative to an injector and provider configuration.

@Injectable({providedIn: 'root'})
export class SessionStore {}

This is the usual application-wide provision. A lower provider can deliberately create a separate scope:

export const routes: Routes = [
  {
    path: 'checkout',
    providers: [CheckoutDraft],
    loadComponent: () => import('./checkout-page'),
  },
];

Components inside that route share the route-scoped CheckoutDraft; unrelated routes do not. A component-level provider creates an instance for that component's element injector and descendants:

@Component({
  selector: 'editable-card',
  providers: [EditBuffer],
  template: '...',
})
export class EditableCard {}

For non-class values, use an InjectionToken instead of a string key:

export interface ApiConfig {
  baseUrl: string;
  timeoutMs: number;
}
 
export const API_CONFIG = new InjectionToken<ApiConfig>('API_CONFIG');
 
export const appConfig: ApplicationConfig = {
  providers: [
    {provide: API_CONFIG, useValue: {baseUrl: '/api', timeoutMs: 5_000}},
  ],
};

A good answer covers the intended owner and lifetime; root EnvironmentInjector, route providers, and component ElementInjector; overrides of the same token; cleanup when the owning injector is destroyed; and InjectionToken or factory providers for configuration and abstractions.

Do not base a 2026 answer only on eagerly versus lazily loaded NgModule providers. That model still matters in NgModule applications, but standalone bootstrap, lazy components, route providers, and environment injectors are central in current Angular.


Mistake 4: Reciting Lifecycle Slogans

Is the constructor only for dependency injection?

No. A JavaScript/TypeScript constructor creates the instance and runs in an injection context, but it is not an Angular lifecycle hook. It can initialize state that does not depend on bound inputs or an initialized view. The real issue is using data before its contract says it exists.

Use each mechanism for its timing:

MechanismAppropriate use
Constructor or field initializerInject dependencies; initialize input-independent state
ngOnChangesReact to initial and later input changes
ngOnInitOne-time initialization after initial inputs are set
ngAfterViewInitWork that needs initialized view queries
afterNextRender / afterEveryRenderDOM work after Angular renders
DestroyRef.onDestroy / ngOnDestroyRelease resources owned by this scope

Input-dependent work that must react to later changes does not belong only in ngOnInit:

export class UserPanel implements OnChanges {
  userId = input.required<string>();
  private api = inject(UserApi);
 
  ngOnChanges(changes: SimpleChanges<UserPanel>) {
    if (changes.userId) {
      this.load(changes.userId.currentValue);
    }
  }
 
  private load(userId: string) {
    // Start or replace input-dependent work.
  }
}

In many current designs, a computed, effect, or RxJS pipeline can express the dependency more directly. Use an effect for side effects, not to copy derivable state between signals.

Avoid memorizing a lifecycle list that omits repeated check phases, render callbacks, projected content, or destruction. In an interview, state the dependency—input, view, DOM render, or owned resource—and select the hook from that.


Mistake 5: Optimizing from a Checklist Instead of Evidence

Why are “OnPush, trackBy, lazy loading” not a performance plan?

Those words do not identify the bottleneck. In Angular 22, OnPush is already the default strategy. Current templates use required track expressions in @for; standalone routes can lazy-load components; @defer can split optional UI; zoneless is already the default for new Angular 21+ applications.

Start with a symptom and metric:

  • slow initial load: JavaScript transfer, LCP, hydration, route or deferred chunks;
  • slow interaction: INP, long tasks, change-detection work, layout or paint;
  • list churn: stable identity, DOM reuse, allocations, virtual scrolling;
  • memory growth: retained components, subscriptions, listeners, caches, and detached DOM;
  • slow server rendering: data waterfalls, serialization, cache policy, and pending tasks.

Use Angular DevTools and browser performance tooling to locate the cost. Then make the smallest relevant change:

@for (account of accounts; track account.id) {
  <account-row [account]="account" />
}
 
@defer (on viewport) {
  <heavy-chart />
} @placeholder {
  <chart-skeleton />
}

Template methods are not automatically bugs, pure pipes do not magically memoize every possible input forever, and runOutsideAngular is not a universal zoneless optimization. Measure the actual computation and rendering frequency.

Route loading is also a trade-off. Lazy loading reduces the initial bundle but adds a later request; eager loading may be right for the primary landing route. Preloading, @defer, SSR, hydration, and image optimization address different bottlenecks.

A strong answer ends with re-measurement and regression protection: a performance budget, representative trace, lab test, field metric, or focused benchmark.


Quick Reference

Weak answerStronger 2026 answer
“OnPush uses ===, so only new references update.”“Input comparison is one trigger; I will identify the version, notification source, and equality boundary.”
“Always unsubscribe in ngOnDestroy.”“The owner and lifetime decide cleanup; use AsyncPipe or takeUntilDestroyed for long-lived streams.”
providedIn: 'root' means exactly one instance.”“It provisions at root, while lower injectors can override or scope the same token.”
“Constructor for DI, all initialization in ngOnInit.”“Choose construction, input, view, render, or destroy timing from the dependency.”
“Use OnPush, trackBy, and lazy loading.”“Profile a named metric, apply a relevant optimization, and re-measure.”

Frequently Asked Questions

What is the biggest Angular interview mistake in 2026?

The biggest mistake is answering from an older Angular mental model without stating the version. Angular 21 made zoneless change detection the default, and Angular 22 made OnPush the default strategy. Explain which version and application mode you mean, then reason from Angular's actual notification mechanisms: inputs, template or host listeners, signals read by templates, AsyncPipe, ComponentRef.setInput, and markForCheck.

Does mutating an object always break Angular OnPush change detection?

No. Mutating an input object without changing its reference does not itself notify an OnPush child, but another valid notification can still cause that view to be checked. Signals also use equality to decide whether a set is a change; mutating a value in place without setting a distinguishable value can leave consumers stale. Prefer explicit immutable updates at component boundaries and explain the notification that makes the view eligible for checking.

Do Angular developers need to unsubscribe from every Observable?

No. Cleanup depends on the Observable's lifetime and the side effect of remaining subscribed. AsyncPipe manages its own subscription, takeUntilDestroyed ties an imperative subscription to DestroyRef, and finite streams such as a normal HttpClient request usually complete. Long-lived event, timer, subject, and store streams need an explicit ownership and cleanup story.

Does providedIn root guarantee one Angular service instance everywhere?

It provides the service through the root environment injector for the usual application scope, but Angular DI is hierarchical. A component, directive, route, lazy NgModule, or another environment injector can provide the same token and create or select a different scoped instance. Describe the intended owner and lifetime instead of using singleton as an unqualified promise.

Should all Angular initialization go in ngOnInit?

No. Construction and field initialization are valid for state that does not depend on bound inputs or an initialized view. ngOnChanges reacts to input changes, ngOnInit runs once after initial inputs are set, afterNextRender or afterEveryRender handles DOM work after rendering, and DestroyRef or ngOnDestroy owns cleanup. Choose the hook from the data and timing contract.

How should I answer Angular performance questions?

Start with a measured symptom and budget, then profile with Angular DevTools and the browser performance tools. Optimize the identified bottleneck using stable tracking in @for, lazy routes, @defer, efficient template work, signals or appropriate change-detection notifications, and SSR or hydration where the product needs them. Re-measure the relevant Core Web Vital, interaction, bundle, or rendering metric instead of reciting a universal checklist.

Sources


Ready to ace your interview?

Get 550+ interview questions with detailed answers in our comprehensive PDF guides.

View PDF Guides