7+ Advanced Angular Interview Questions 2025: Change Detection, Signals & Zone.js

·11 min read
AngularInterview QuestionsAdvanced AngularChange DetectionZone.jsDependency InjectionAngular SignalsSenior Developer

Angular is the framework of choice for 17% of web developers and dominates enterprise applications—yet Zone.js, the library that makes Angular's "magic" work, is understood by fewer than 1 in 10 Angular developers. How does Angular know when your data changed? Why do some components update and others don't? What actually happens when you call markForCheck()?

The questions that separate senior Angular developers from experienced beginners aren't about syntax or APIs. They're about understanding the mechanisms that make Angular work.

Table of Contents

  1. Change Detection Questions
  2. Zone.js Questions
  3. Change Detection Strategy Questions
  4. Dependency Injection Questions
  5. Angular Signals Questions
  6. ChangeDetectorRef Questions
  7. Standalone Components Questions

Change Detection Questions

These questions test your understanding of Angular's core update mechanism.

How does change detection work in Angular?

Change detection is Angular's process of comparing the current component state with the previous state and updating the DOM when differences are found. It runs whenever async operations complete—like HTTP responses, timers, or user events—because Angular uses Zone.js to intercept these operations and trigger checks automatically.

A common misconception is that Angular uses a Virtual DOM like React. It doesn't. Angular uses Zone.js to know when to check for changes and a component tree traversal to detect what changed.

Here's the mental model: Zone.js wraps browser APIs like setTimeout, addEventListener, and fetch. When any async operation completes inside the Angular zone, Zone.js notifies Angular, which then runs change detection.

During change detection, Angular walks down the component tree from root to leaves. For each component, it compares the current values of template expressions with their previous values. If something changed, Angular updates the DOM.

// This triggers change detection automatically
@Component({
    selector: 'app-counter',
    template: `<button (click)="increment()">{{ count }}</button>`
})
export class CounterComponent {
    count = 0;
 
    increment() {
        this.count++; // Zone.js detects the click, triggers CD
    }
}

The default strategy checks every component on every cycle—even if nothing changed. This is why OnPush exists, and why understanding change detection matters for performance.

How would you trigger change detection manually?

Use ChangeDetectorRef.detectChanges() for immediate detection and markForCheck() for scheduling in OnPush components.


Zone.js Questions

These questions reveal whether you understand Angular's internals or just use it as a black box.

What is Zone.js and can you run Angular without it?

Zone.js is a library that patches async browser APIs to create an execution context where Angular can detect when async operations complete. Yes, you can run Angular without Zone.js using the zoneless change detection option introduced in Angular 18, but you'll need to manage change detection manually or use Signals.

Zone.js monkey-patches browser APIs—setTimeout, Promise, addEventListener, fetch, XMLHttpRequest, and others—to intercept when they're called and when they complete.

Think of Zone.js as a surveillance system for async operations. Every time you click a button, wait for an HTTP response, or use setTimeout, Zone.js knows about it and tells Angular "something async just happened—you might want to check for changes."

// Without Zone.js, this wouldn't trigger change detection
setTimeout(() => {
    this.message = 'Updated!'; // Zone.js intercepts setTimeout
}, 1000);

Since Angular 18, you can opt out of Zone.js entirely:

// main.ts - Zoneless Angular
bootstrapApplication(AppComponent, {
    providers: [
        provideExperimentalZonelessChangeDetection()
    ]
});

Without Zone.js, Angular doesn't automatically know when to run change detection. You either manage it manually with ChangeDetectorRef, or you use Angular Signals, which have their own reactivity system.

How do you run code outside the Angular zone?

The NgZone service lets you interact with zones programmatically:

export class HeavyComponent {
    constructor(private ngZone: NgZone) {}
 
    runHeavyTask() {
        // Run outside Angular zone - no change detection triggered
        this.ngZone.runOutsideAngular(() => {
            // Heavy computation or third-party library
            heavyLibrary.process();
        });
 
        // Re-enter Angular zone when you need updates
        this.ngZone.run(() => {
            this.result = 'Done!';
        });
    }
}

Change Detection Strategy Questions

These questions test performance optimization knowledge and immutability understanding.

What is the difference between OnPush and Default change detection strategies?

Default strategy checks the component on every change detection cycle. OnPush only checks when input references change, events originate from the component, or you explicitly trigger detection. Use OnPush with immutable data patterns for better performance; use Default when component data changes frequently from external sources.

The strategies differ in when Angular checks a component, and this affects how you need to structure your data flow.

With Default, Angular checks the component during every change detection cycle, regardless of whether its inputs changed. Safe but potentially wasteful.

With OnPush, Angular only checks the component when:

  • An @Input() reference changes (not just its contents)
  • An event originates from the component or its children
  • You explicitly call markForCheck() or detectChanges()
  • An async pipe receives a new value
@Component({
    selector: 'app-user-card',
    changeDetection: ChangeDetectionStrategy.OnPush,
    template: `<div>{{ user.name }}</div>`
})
export class UserCardComponent {
    @Input() user!: User;
}
 
// Parent component
@Component({
    template: `<app-user-card [user]="currentUser" />`
})
export class ParentComponent {
    currentUser = { name: 'Alice' };
 
    updateName() {
        // WRONG: OnPush won't detect this mutation
        this.currentUser.name = 'Bob';
 
        // CORRECT: Create new reference
        this.currentUser = { ...this.currentUser, name: 'Bob' };
    }
}

How does OnPush work with RxJS Observables?

OnPush pairs perfectly with RxJS Observables and the async pipe:

@Component({
    changeDetection: ChangeDetectionStrategy.OnPush,
    template: `
        <div *ngIf="user$ | async as user">
            {{ user.name }}
        </div>
    `
})
export class UserCardComponent {
    user$ = this.userService.getUser();
}

The async pipe automatically calls markForCheck() when new values arrive, making OnPush work seamlessly with reactive patterns.


Dependency Injection Questions

These questions test understanding of Angular's DI system beyond basic usage.

How does hierarchical dependency injection work?

Angular creates a tree of injectors that mirrors the component tree. When you request a dependency, Angular searches upward through parent injectors until it finds a provider. This enables scoped instances—different parts of your app can have different instances of the same service based on where the provider is registered.

Angular has multiple injector levels, and where you provide a service determines its scope and lifetime.

At the top is the root injector (created by providedIn: 'root'). Services here are true singletons—one instance for the entire application.

Below that are module injectors for eagerly-loaded modules. But here's a gotcha: lazy-loaded modules get their own injector, so services provided in a lazy module are scoped to that module.

Finally, each component can have its own element injector:

// Singleton for the entire app
@Injectable({ providedIn: 'root' })
export class GlobalService { }
 
// New instance for each UserProfileComponent AND its children
@Component({
    selector: 'app-user-profile',
    providers: [UserStateService], // Component-level provider
    template: `...`
})
export class UserProfileComponent { }

How does the injector resolution algorithm work?

The resolution algorithm walks up the injector tree:

UserDetailsComponent (child)
    ↓ "Do I have UserStateService?" No, check parent
UserProfileComponent (parent)
    ↓ "Do I have UserStateService?" Yes! Return this instance

This enables powerful patterns. Providing a state service at a "container" component level means all child components share the same state instance:

@Component({
    selector: 'app-checkout',
    providers: [CheckoutStateService], // Scoped to checkout flow
    template: `
        <app-cart />      <!-- Gets same CheckoutStateService -->
        <app-payment />   <!-- Gets same CheckoutStateService -->
        <app-summary />   <!-- Gets same CheckoutStateService -->
    `
})
export class CheckoutComponent { }

What is the difference between providedIn: 'root' and AppModule providers?

Tree shaking. With providedIn: 'root', unused services are removed during build. With AppModule providers, they're always included.


Angular Signals Questions

These questions test awareness of modern Angular and where the framework is heading.

What are Angular Signals and how do they change reactivity?

Signals are reactive primitives introduced in Angular 16 that track value changes without Zone.js. When a signal's value changes, Angular knows exactly which components need updating—no tree traversal required. Signals enable fine-grained reactivity and are the foundation for zoneless Angular.

Signals represent a fundamental shift in how Angular handles reactivity. Unlike Zone.js-based change detection (which checks everything when anything might have changed), signals enable fine-grained reactivity where Angular knows exactly what changed.

A signal is a wrapper around a value that tracks reads and writes:

import { signal, computed, effect } from '@angular/core';
 
@Component({
    selector: 'app-counter',
    template: `
        <p>Count: {{ count() }}</p>
        <p>Double: {{ doubled() }}</p>
        <button (click)="increment()">+1</button>
    `
})
export class CounterComponent {
    // Writable signal
    count = signal(0);
 
    // Computed signal - automatically updates when count changes
    doubled = computed(() => this.count() * 2);
 
    increment() {
        // Update signal value
        this.count.update(c => c + 1);
    }
}

Notice the template uses count() not count—you call the signal like a function to read its value. This is how Angular tracks which templates depend on which signals.

How do Signals integrate with RxJS?

Signals work with RxJS via toSignal and toObservable:

@Component({...})
export class UserComponent {
    // Convert Observable to Signal
    user = toSignal(this.userService.getCurrentUser());
 
    // Convert Signal to Observable
    count = signal(0);
    count$ = toObservable(this.count);
}

ChangeDetectorRef Questions

These questions catch developers who've used these methods without understanding the difference.

When do you use markForCheck() vs detectChanges()?

markForCheck() schedules the component for checking during the next change detection cycle—it's asynchronous. detectChanges() immediately runs change detection on the component and its children—it's synchronous. Use markForCheck() with OnPush to notify Angular that something changed; use detectChanges() when you need immediate DOM updates.

markForCheck() marks the component and all its ancestors up to the root as "dirty"—needing to be checked. But it doesn't run change detection immediately. It waits for the next cycle:

@Component({
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class NotificationComponent {
    message = '';
 
    constructor(
        private cdr: ChangeDetectorRef,
        private websocket: WebSocketService
    ) {
        // WebSocket messages happen outside Angular zone
        this.websocket.messages$.subscribe(msg => {
            this.message = msg;
            this.cdr.markForCheck(); // Schedule for next CD cycle
        });
    }
}

detectChanges() immediately runs change detection on this component and its children, synchronously:

export class ChartComponent implements AfterViewInit {
    constructor(private cdr: ChangeDetectorRef) {}
 
    ngAfterViewInit() {
        // Third-party chart library updates the DOM
        this.chartLibrary.render(this.chartElement);
 
        // Immediately sync Angular's view with the DOM
        this.cdr.detectChanges();
    }
}

What are the differences in timing and scope?

MethodTimingScope
markForCheck()Next CD cycleMarks ancestors for checking
detectChanges()ImmediateChecks component + descendants

A common mistake is using detectChanges() when markForCheck() would suffice. detectChanges() can cause performance issues if called frequently, and it can lead to "Expression has changed after it was checked" errors if you're not careful about timing.


Standalone Components Questions

These questions test whether candidates are keeping up with Angular's evolution.

What are standalone components and why are they the future?

Standalone components declare their dependencies directly in the component decorator instead of through NgModules. They simplify Angular architecture by removing the need for module declarations and making components more self-contained. They're the recommended approach since Angular 15 and are required for zoneless applications.

Instead of the traditional NgModule → declares → Component pattern, standalone components are self-contained units that declare their own dependencies:

// Traditional approach with NgModule
@NgModule({
    declarations: [UserCardComponent],
    imports: [CommonModule, SharedModule],
    exports: [UserCardComponent]
})
export class UserModule { }
 
// Standalone approach
@Component({
    selector: 'app-user-card',
    standalone: true,
    imports: [CommonModule, DatePipe, RouterLink],
    template: `...`
})
export class UserCardComponent { }

What are the advantages of standalone components?

The advantages go beyond just less boilerplate. Standalone components are more tree-shakable because dependencies are explicit at the component level. They're easier to lazy-load:

// Lazy-load a standalone component directly
const routes: Routes = [
    {
        path: 'admin',
        loadComponent: () => import('./admin/admin.component')
            .then(m => m.AdminComponent)
    }
];

How do you bootstrap an application without NgModule?

For new applications, you can bootstrap without any NgModule:

// main.ts - No AppModule needed
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { provideRouter } from '@angular/router';
 
bootstrapApplication(AppComponent, {
    providers: [
        provideRouter(routes),
        provideHttpClient()
    ]
});

Standalone components aren't just about removing modules—they're about making Angular applications more modular, tree-shakable, and aligned with modern JavaScript patterns. Combined with signals and zoneless change detection, they represent Angular's modernized architecture.


Ready to ace your interview?

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

View PDF Guides