Advanced Angular Interview Questions

This document provides a comprehensive list of medium to advanced interview questions for experienced Angular developers. The questions cover a wide range of topics, from core architectural concepts to performance optimization and modern features.

Core Concepts & Architecture

1. What is the difference between AOT (Ahead-of-Time) and JIT (Just-in-Time) compilation? What are the advantages of AOT?

Answer:

  • JIT (Just-in-Time) Compilation: This is the default compilation mode in development. The browser downloads the Angular framework, your application code, and the Angular compiler. When the application starts, the JIT compiler compiles the components and templates in the browser.
  • AOT (Ahead-of-Time) Compilation: In this mode, the Angular application is pre-compiled on the server as part of the build process. The browser only downloads the compiled application, which is plain JavaScript.

Advantages of AOT:

  • Faster Rendering: The application is already compiled, so the browser can render it immediately without waiting for the compilation step.
  • Smaller Application Size: The Angular compiler is not shipped to the browser, which significantly reduces the bundle size.
  • Template Error Detection: Template errors are detected during the build process, not at runtime in the user’s browser.
  • Improved Security: AOT compilation inlines external HTML templates and CSS, reducing the risk of injection attacks.

2. Explain the role of zone.js in Angular.

Answer:

zone.js is a library that creates execution contexts (zones) for asynchronous operations. Angular uses NgZone, which is a forked zone from the global zone, to automatically trigger change detection when asynchronous tasks complete.

Essentially, zone.js patches most of the browser’s asynchronous APIs (like setTimeout, Promise, and event listeners). When an async operation starts, it enters the NgZone, and when it completes, NgZone notifies Angular, which then runs its change detection mechanism to update the UI. This is why you don’t have to manually trigger updates after an HTTP request or a timer.

3. What are entryComponents and why are they deprecated?

Answer:

In older versions of Angular (before Ivy), entryComponents was an array in the @NgModule decorator that you used to tell the compiler which components would be created and inserted into the DOM dynamically (e.g., with ViewContainerRef.createComponent()). This was necessary because the compiler needed to generate factories for these components.

With the introduction of the Ivy rendering engine, this is no longer needed. Ivy is “locally-aware” and can compile components on-demand without needing to know about them in advance. Any component can now be dynamically loaded.

4. Explain the concept of a “monorepo” and how tools like Nx can be beneficial for Angular development.

Answer:

A monorepo is a single repository that stores the code for multiple projects. For an Angular application, this could mean having your frontend app, a shared library of components, and even your backend API all in the same repository.

Benefits of using a monorepo with tools like Nx:

  • Shared Code: It’s easy to share code (like interfaces, services, or components) between different projects.
  • Atomic Commits: A single commit can span multiple projects, making it easier to coordinate changes.
  • Simplified Dependency Management: All projects use the same version of third-party libraries.
  • Improved Tooling: Tools like Nx provide features like smart builds (only rebuilding what’s affected by a change), code generation, and dependency graphs.

5. What is Angular Universal and what problem does it solve?

Answer:

Angular Universal is a technology that allows you to run your Angular application on the server, a process known as Server-Side Rendering (SSR).

It solves two main problems:

  1. Improved Performance for First-Time Users: With SSR, the server sends a fully rendered HTML page to the browser. The user sees the content immediately, instead of a blank page while the client-side app loads. This improves the perceived performance and metrics like First Contentful Paint (FCP).
  2. Better SEO: Search engine crawlers can better understand and index your site because the content is present in the initial HTML payload, rather than being generated by JavaScript on the client side.

Components & Directives

6. What is the difference between a Component and a Directive?

Answer:

  • Component: A component is a directive with a template. It’s the main building block of an Angular UI. Components are always associated with a view. You declare them with @Component.
  • Directive: A directive is a class that adds new behavior to elements in the template. It doesn’t have its own view. There are two types:
    • Structural Directives: Change the DOM layout by adding or removing elements (e.g., *ngIf, *ngFor). They are prefixed with an asterisk *.
    • Attribute Directives: Change the appearance or behavior of an element, component, or another directive (e.g., ngClass, ngStyle).

7. Explain the difference between ViewChild and ContentChild.

Answer:

Both are decorators used to query for elements in the DOM. The key difference is the timing and the part of the DOM they query.

  • @ViewChild / @ViewChildren: Queries for elements within the component’s own template (its “view”). The results are available in the ngAfterViewInit lifecycle hook.
  • @ContentChild / @ContentChildren: Queries for elements that are projected into the component’s template using <ng-content>. These elements are defined by the parent component. The results are available in the ngAfterContentInit lifecycle hook.

8. How would you create a custom structural directive?

Answer:

To create a structural directive, you need to inject TemplateRef and ViewContainerRef.

  • TemplateRef: Represents the template content (<ng-template>) that the directive is attached to.
  • ViewContainerRef: A container where you can attach or detach views.

Here’s a simple example of a custom if directive:

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[appMyIf]'
})
export class MyIfDirective {
  private hasView = false;

  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
  ) { }

  @Input() set appMyIf(condition: boolean) {
    if (condition && !this.hasView) {
      this.viewContainer.createEmbeddedView(this.templateRef);
      this.hasView = true;
    } else if (!condition && this.hasView) {
      this.viewContainer.clear();
      this.hasView = false;
    }
  }
}

Usage:

<div *appMyIf="someCondition">
  This will be shown if someCondition is true.
</div>

9. What is the purpose of the HostBinding and HostListener decorators?

Answer:

  • @HostListener(eventName, [args]): A decorator that declares a DOM event to listen for on the host element of a component or directive. It provides a handler method to run when that event occurs.
  • @HostBinding(hostPropertyName): A decorator that marks a DOM property as a host-binding property. Angular will automatically check for changes to this property during change detection and update the host element.

Example: A directive that highlights an element on mouseover.

import { Directive, HostListener, HostBinding } from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {
  @HostBinding('style.backgroundColor') backgroundColor: string;

  @HostListener('mouseenter') onMouseEnter() {
    this.backgroundColor = 'yellow';
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.backgroundColor = 'transparent';
  }
}

10. Explain the component lifecycle hooks in order of execution.

Answer:

  1. ngOnChanges: Called before ngOnInit and whenever one or more data-bound input properties change.
  2. ngOnInit: Called once, after the first ngOnChanges. Used for component initialization.
  3. ngDoCheck: Called during every change detection run, immediately after ngOnChanges and ngOnInit. Used for custom change detection.
  4. ngAfterContentInit: Called once after the first ngDoCheck. Called after Angular projects external content into the component’s view.
  5. ngAfterContentChecked: Called after ngAfterContentInit and every subsequent ngDoCheck.
  6. ngAfterViewInit: Called once after the first ngAfterContentChecked. Called after the component’s views and child views are initialized.
  7. ngAfterViewChecked: Called after ngAfterViewInit and every subsequent ngAfterContentChecked.
  8. ngOnDestroy: Called once, just before the directive is destroyed. Used for cleanup.

11. How can you create a dynamic component?

Answer:

You can create components dynamically at runtime using ViewContainerRef.

import { Component, ViewChild, ViewContainerRef, ComponentFactoryResolver } from '@angular/core';
import { MyDynamicComponent } from './my-dynamic.component';

@Component({
  selector: 'app-root',
  template: `<ng-template #dynamicComponentContainer></ng-template>`
})
export class AppComponent {
  @ViewChild('dynamicComponentContainer', { read: ViewContainerRef, static: true }) container: ViewContainerRef;

  constructor(private resolver: ComponentFactoryResolver) {}

  ngOnInit() {
    const factory = this.resolver.resolveComponentFactory(MyDynamicComponent);
    const componentRef = this.container.createComponent(factory);
    componentRef.instance.someInput = 'Hello from dynamic component!';
  }
}

Note: With Ivy, ComponentFactoryResolver is becoming less necessary, and you can pass the component type directly to createComponent.

Dependency Injection (DI)

12. Explain the hierarchical nature of Angular’s DI system.

Answer:

Angular has a hierarchical dependency injection system. This means that if a component requests a dependency, Angular first tries to satisfy that dependency with its own injector. If the component’s injector doesn’t have a provider for the requested dependency, it passes the request up to its parent component’s injector, and so on, up to the root injector.

This allows you to have different instances of a service at different levels of the component tree.

13. What is the difference between providedIn: 'root', providedIn: MyModule, and providing a service in a component’s providers array?

Answer:

  • providedIn: 'root': This is the most common and recommended way to provide a service. It registers the service with the root injector, making it a singleton available to the entire application. It’s also tree-shakable, meaning if the service is never used, it will be removed from the final bundle.
  • providedIn: MyModule: This registers the service with a specific module’s injector. The service will be a singleton within the scope of that module. This is useful for services that are only relevant to a specific feature module.
  • Component providers array: Providing a service in a component’s providers array creates a new instance of that service for that component and its children. This is useful when you need a separate instance of a service for each instance of a component.

14. What are the @Injectable, @Inject, @Optional, and @Self decorators used for in DI?

Answer:

  • @Injectable(): Marks a class as available to be provided and injected as a dependency. It’s technically only required if the class itself has dependencies to be injected.
  • @Inject(TOKEN): A manual way to specify the dependency token. This is useful when the dependency is not a class, but a string, an object, or an InjectionToken.
  • @Optional(): Tells Angular that a dependency is optional. If the dependency can’t be resolved, Angular will inject null instead of throwing an error.
  • @Self(): Tells the injector to only look for a provider in the component’s own injector, and not to bubble up the injector tree.

15. What is an InjectionToken and when would you use it?

Answer:

An InjectionToken is used to create a unique token for a dependency that is not a class. This is useful for injecting primitive values (like strings or numbers) or configuration objects.

Example:

// app.config.ts
import { InjectionToken } from '@angular/core';

export const APP_CONFIG = new InjectionToken<any>('app.config');

// app.module.ts
providers: [
  { provide: APP_CONFIG, useValue: { apiEndpoint: 'https://api.example.com' } }
]

// my.service.ts
import { Inject } from '@angular/core';
import { APP_CONFIG } from './app.config';

constructor(@Inject(APP_CONFIG) private config: any) {
  console.log(config.apiEndpoint);
}

16. Explain the purpose of useClass, useValue, useFactory, and useExisting in provider configuration.

Answer:

These are different ways to configure a provider in the providers array:

  • useClass: The default. The injector will instantiate a new instance of the provided class. { provide: Logger, useClass: Logger }
  • useValue: Provides a static value. Useful for configuration objects or constants. { provide: TITLE, useValue: 'My App' }
  • useFactory: Provides a value by invoking a factory function. This is useful when the creation of the dependency is complex or depends on other services. { provide: ApiService, useFactory: apiFactory, deps: [HttpClient] }
  • useExisting: Creates an alias for an existing token. This is useful for creating a second API to access the same service instance. { provide: OldLogger, useExisting: NewLogger }

17. What is the forwardRef function and why is it needed?

Answer:

forwardRef allows you to refer to a class that has not yet been defined. This is needed in situations where you have a circular dependency. For example, if ServiceA depends on ServiceB, and ServiceB depends on ServiceA. By wrapping the reference in forwardRef, you defer the evaluation of the token until runtime, when both classes will have been defined.

18. How does the multi: true option work in providers?

Answer:

The multi: true option allows you to provide multiple values for a single dependency token. The injector will then inject an array of all the provided values. This is commonly used for features like HTTP_INTERCEPTORS.

Example:

providers: [
  { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
  { provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true }
]

A component that injects HTTP_INTERCEPTORS will receive an array containing instances of both AuthInterceptor and LoggingInterceptor.

Change Detection

19. Explain the difference between the Default and OnPush change detection strategies.

Answer:

  • ChangeDetectionStrategy.Default: This is the default strategy. With this strategy, Angular will run change detection on a component whenever any change happens in the application (e.g., a user event, a timer, an HTTP response). Angular will check all components in the component tree from top to bottom.
  • ChangeDetectionStrategy.OnPush: This is a performance optimization strategy. When a component uses OnPush, Angular will only run change detection on it if one of the following occurs:
    1. One of its @Input() properties changes (the reference must change for objects/arrays).
    2. An event handler in the component (or one of its children) is triggered.
    3. You manually trigger change detection using ChangeDetectorRef.
    4. An async pipe in its template emits a new value.

20. When using OnPush, how can you manually trigger change detection?

Answer:

You can inject ChangeDetectorRef into your component and use its methods:

  • markForCheck(): This is the preferred method. It marks the component and all its ancestors as “dirty” (needing a check). Change detection will then run on the next tick. It does not trigger change detection immediately.
  • detectChanges(): This triggers change detection for the current component and its children immediately. This should be used with caution as it can lead to performance issues if overused.

21. What does it mean for data to be “immutable” and why is it important for OnPush strategy?

Answer:

Immutability means that once an object or array is created, it cannot be changed. If you need to modify it, you create a new object or array with the updated values.

This is crucial for OnPush change detection because Angular’s change detection mechanism for @Input() properties does a simple reference check (===). If you mutate an object (e.g., myObject.property = 'new value'), the reference to the object doesn’t change, so Angular won’t detect the change and won’t update the view.

By using immutable data, you ensure that any change results in a new object reference, which OnPush can detect.

// BAD: Mutation - OnPush won't detect this
this.user.name = 'new name';

// GOOD: Immutability - OnPush will detect this
this.user = { ...this.user, name: 'new name' };

22. How does the async pipe work with change detection?

Answer:

The async pipe is a powerful tool that works seamlessly with the OnPush strategy. It subscribes to an Observable or Promise and returns the latest value it has emitted.

When a new value is emitted, the async pipe automatically calls markForCheck() on the component, which schedules a change detection run. It also automatically unsubscribes when the component is destroyed, preventing memory leaks.

23. What is ngZone.runOutsideAngular() and when would you use it?

Answer:

ngZone.runOutsideAngular() allows you to run code that won’t trigger Angular’s change detection. This is a performance optimization technique used for frequent, high-cost asynchronous operations that don’t need to update the UI on every tick.

A common use case is setting up a mousemove event listener that performs complex calculations. You can run the calculations outside the Angular zone and then use ngZone.run() to re-enter the zone and update the UI only when necessary.

24. Can you explain what Coalescing is in the context of Angular change detection?

Answer:

Event coalescing is an optimization in Angular where multiple events that occur in the same execution frame can be “coalesced” into a single change detection cycle. For example, if you have a (click) event that triggers multiple state changes rapidly, Angular might not run change detection for every single change, but rather batch them together to improve performance. This is handled internally by NgZone.

RxJS & State Management

25. What is the difference between an Observable and a Promise?

Answer:

FeaturePromiseObservable
ValuesEmits a single value (or a rejection).Can emit multiple values over time.
ExecutionEager (executes immediately upon creation).Lazy (executes only when subscribed to).
CancellableNoYes (by unsubscribing).
OperatorsLimited (.then(), .catch(), .finally()).Rich set of operators (map, filter, reduce, etc.).

26. Explain the difference between switchMap, mergeMap, concatMap, and exhaustMap.

Answer:

These are higher-order mapping operators in RxJS. They are used when you have an outer observable that emits values, and for each of those values, you want to create a new inner observable.

  • switchMap: Subscribes to the inner observable. If the outer observable emits a new value, it unsubscribes from the previous inner observable and switches to the new one. Useful for scenarios like type-ahead search, where you only care about the latest request.
  • mergeMap (or flatMap): Subscribes to all inner observables and emits their values as they arrive. It runs them in parallel. Useful when you need to handle multiple requests concurrently.
  • concatMap: Subscribes to inner observables in sequence. It waits for the current inner observable to complete before subscribing to the next one. Useful for when the order of operations is important.
  • exhaustMap: Subscribes to an inner observable. If the outer observable emits a new value while the current inner observable is still active, it ignores the new value. Useful for scenarios like login buttons, where you want to prevent multiple clicks while the first request is in progress.

27. What is a Subject in RxJS? Explain the different types of Subjects.

Answer:

A Subject is a special type of Observable that is also an Observer. This means you can both subscribe to it and manually push new values into it using the .next() method. It’s a multicast, meaning it shares a single execution path among all subscribers.

Types of Subjects:

  • Subject: The standard subject. It does not have an initial value. Subscribers will only receive values that are emitted after they have subscribed.
  • BehaviorSubject: Requires an initial value. It stores the “current” value and emits it to any new subscribers immediately upon subscription.
  • ReplaySubject: Can “replay” a specified number of previously emitted values to new subscribers. It’s like a buffer.
  • AsyncSubject: Only emits the very last value from the observable stream, and only when the stream completes.

28. How do you prevent memory leaks with Observables in Angular components?

Answer:

  1. async pipe: This is the easiest and safest way. The pipe handles both subscribing and unsubscribing automatically.
  2. takeUntil operator: Create a Subject that emits a value in ngOnDestroy. Pipe your observables through takeUntil(this.destroy$). This is a clean and declarative way to manage subscriptions.private destroy$ = new Subject<void>(); ngOnInit() { this.myObservable$.pipe(takeUntil(this.destroy$)).subscribe(...); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); }
  3. Manual unsubscribe(): Store the Subscription object and call .unsubscribe() in ngOnDestroy. This is more imperative and can be error-prone if you have many subscriptions.

29. What is NgRx and what are its core principles?

Answer:

NgRx is a state management library for Angular, inspired by Redux. It provides a way to manage global state in a predictable and maintainable way, especially in large applications.

Core Principles:

  1. Single Source of Truth: The state of your entire application is stored in an object tree within a single Store.
  2. State is Read-Only: The only way to change the state is to dispatch an Action, an object describing what happened.
  3. Changes are Made with Pure Functions: To specify how the state tree is transformed by actions, you write pure Reducers. A reducer is a function that takes the previous state and an action, and returns the next state.

30. What is the role of “Effects” in NgRx?

Answer:

Effects are used to handle side effects in NgRx, such as asynchronous operations like fetching data from an API. An Effect listens for dispatched actions, performs some task (like making an HTTP request), and then dispatches a new action (e.g., a success or failure action) with the result. This keeps your components and reducers pure and free of side effects.

31. What are “Selectors” in NgRx?

Answer:

Selectors are pure functions used to get slices of data from the store state. They provide a way to compute derived data, and they are memoized. This means that if the input to a selector hasn’t changed, it will return the previously computed value without re-running the computation, which is a powerful performance optimization.

32. Compare and contrast NgRx with a simple service-based state management using a BehaviorSubject.

Answer:

  • Service with BehaviorSubject:
    • Pros: Simple to set up, minimal boilerplate, good for small to medium-sized applications or for managing local component state.
    • Cons: Can become messy in large applications with complex state interactions. Less predictable, as any part of the app with access to the service can call .next() and change the state. Lacks the powerful tooling (like time-travel debugging) of NgRx.
  • NgRx:
    • Pros: Enforces a unidirectional data flow, making state changes predictable and traceable. Excellent dev tools (Redux DevTools). Well-suited for large, complex applications with many developers. Selectors provide memoization for performance.
    • Cons: Significant boilerplate and a steep learning curve. Can be overkill for simple applications.

Forms

33. What are the differences between Template-Driven Forms and Reactive Forms?

Answer:

FeatureTemplate-Driven FormsReactive Forms
SetupMinimal setup in the component class. Logic is in the template.More setup in the component class. The “source of truth” is in the class.
Data ModelUnstructured. Uses ngModel.Structured. Uses FormControl, FormGroup, FormArray.
MutabilityAsynchronous.Synchronous.
ValidationValidation logic is defined using directives in the template.Validation logic is defined as functions in the component class.
TestabilityHarder to unit test.Easier to unit test, as the form model is accessible in the class.
Use CaseGood for simple forms like login or contact forms.Good for complex, dynamic forms where you need more control.

34. How do you handle dynamic forms (e.g., adding/removing form controls) in Reactive Forms?

Answer:

You use a FormArray. A FormArray is a way to manage a collection of FormControls, FormGroups, or other FormArrays. You can dynamically add or remove controls from the array.

Example:

// In the component class
form = new FormGroup({
  name: new FormControl(''),
  aliases: new FormArray([])
});

get aliases() {
  return this.form.get('aliases') as FormArray;
}

addAlias() {
  this.aliases.push(new FormControl(''));
}

// In the template
<div formArrayName="aliases">
  <div *ngFor="let alias of aliases.controls; let i=index">
    <input [formControlName]="i">
  </div>
</div>

35. What is the purpose of valueChanges and statusChanges in Reactive Forms?

Answer:

valueChanges and statusChanges are Observable properties on AbstractControl (the base class for FormControl, FormGroup, and FormArray).

  • valueChanges: An observable that emits an event every time the value of the control changes. This is useful for reacting to user input in real-time.
  • statusChanges: An observable that emits an event every time the validation status of the control changes (e.g., from PENDING to VALID or INVALID).

36. How do you create a custom validator for a Reactive Form?

Answer:

A custom validator is simply a function that takes a form control as an argument and returns either null if the control is valid, or an error object if it’s invalid.

Example: A validator that checks for a specific forbidden name.

import { AbstractControl, ValidationErrors } from '@angular/forms';

export function forbiddenNameValidator(forbiddenName: string): (control: AbstractControl) => ValidationErrors | null {
  return (control: AbstractControl): ValidationErrors | null => {
    const forbidden = control.value === forbiddenName;
    return forbidden ? { forbiddenName: { value: control.value } } : null;
  };
}

// Usage
name = new FormControl('', [Validators.required, forbiddenNameValidator('admin')]);

37. How can you update the value of a form control? What’s the difference between setValue and patchValue?

Answer:

You can update the value of a FormGroup or FormArray using setValue or patchValue.

  • setValue(): This method requires you to provide a value for every control in the form group. If you miss one, it will throw an error. It’s stricter.
  • patchValue(): This method allows you to update only a subset of the controls in the form group. It’s more flexible.

Routing

38. What are Route Guards and what are the different types?

Answer:

Route Guards are services that implement a specific interface to control navigation to and from routes. They are used to protect routes, for example, by checking if a user is logged in or has certain permissions.

Types of Guards:

  • CanActivate: Controls if a route can be activated.
  • CanActivateChild: Controls if the children of a route can be activated.
  • CanDeactivate: Controls if a user can navigate away from a route (e.g., to prevent leaving a form with unsaved changes).
  • Resolve: Used to pre-fetch data for a route before it’s activated.
  • CanLoad: Controls if a lazy-loaded module can be loaded.

39. Explain how lazy loading of modules works in Angular.

Answer:

Lazy loading is a design pattern where you load feature modules only when the user navigates to their routes. This improves the initial load time of the application because the main bundle doesn’t include the code for these feature modules.

You configure lazy loading in the RouterModule by using the loadChildren property in a route definition.

// app-routing.module.ts
const routes: Routes = [
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
  }
];

When a user navigates to a /admin route, Angular will asynchronously fetch and load the AdminModule.

40. How can you pass data to a route?

Answer:

There are several ways:

  1. Route Parameters: For required data that is part of the URL path.
    • Route config: { path: 'user/:id', component: UserComponent }
    • Access: this.route.snapshot.paramMap.get('id') or this.route.paramMap.subscribe(...)
  2. Query Parameters: For optional parameters.
    • URL: /search?q=angular
    • Access: this.route.snapshot.queryParamMap.get('q') or this.route.queryParamMap.subscribe(...)
  3. Route Data: For static data that you define in the route configuration.
    • Route config: { path: 'about', component: AboutComponent, data: { title: 'About Us' } }
    • Access: this.route.snapshot.data['title']

41. What is the purpose of a Resolver in routing?

Answer:

A Resolver is a route guard that pre-fetches data for a route before the route is activated. This is useful when you want to ensure that the data required by a component is available before the component is rendered, which can prevent a “flicker” effect or the need for loading spinners within the component itself.

The resolver service must implement the Resolve interface, which has a resolve() method. This method can return a Promise, an Observable, or a static value. The router will wait for the data to be resolved before activating the route.

42. What is the difference between routerLink and router.navigate()?

Answer:

  • routerLink: A directive used in the template to create navigation links. It’s the declarative way to navigate. <a routerLink="/users">Users</a>
  • router.navigate(): A method on the Router service used in the component class to navigate programmatically. It’s the imperative way to navigate. this.router.navigate(['/users']);

You would use router.navigate() when you need to perform some logic before navigating, for example, after a form submission.

43. How do you handle wildcard routes and route redirects?

Answer:

  • Wildcard Route: A route with a path of ** will match any URL that doesn’t match the other routes. It’s commonly used for displaying a “404 Not Found” page. It should always be the last route in your configuration. { path: '**', component: PageNotFoundComponent }
  • Redirects: You can configure a route to redirect to another route using redirectTo. You also need to specify a pathMatch strategy.
    • pathMatch: 'full': Redirects if the entire URL path matches.
    • pathMatch: 'prefix': Redirects if the URL path starts with the specified path. (This is the default, but 'full' is often safer for redirects). { path: '', redirectTo: '/dashboard', pathMatch: 'full' }

Performance Optimization

44. What are some key strategies for optimizing the performance of an Angular application?

Answer:

  1. Lazy Loading: Load feature modules on demand.
  2. AOT Compilation: Use Ahead-of-Time compilation for production builds.
  3. OnPush Change Detection: Use the OnPush strategy for components to reduce the number of change detection cycles.
  4. Immutable Data: Use immutable data structures, especially with OnPush.
  5. trackBy Function: Use trackBy with *ngFor to help Angular track items in a list and avoid re-rendering the entire list when it changes.
  6. async Pipe: Use the async pipe to handle subscriptions, as it’s efficient and prevents memory leaks.
  7. Optimize Assets: Minify and compress your code, and optimize images.
  8. Service Workers: Use a service worker to cache application assets and enable offline capabilities.

45. Explain the purpose of the trackBy function in *ngFor.

Answer:

When you use *ngFor to loop over a collection, if the collection changes, Angular by default has no way of knowing which items have been added, removed, or moved. So, it tears down the entire list in the DOM and rebuilds it.

The trackBy function provides a way to give each item in the list a unique identifier. When the list changes, Angular can use this identifier to “track” the items. It will then only add, remove, or move the specific DOM elements that have changed, which is much more efficient than re-rendering the whole list.

Example:

// In the component class
trackByItemId(index: number, item: any): number {
  return item.id;
}

// In the template
<div *ngFor="let item of items; trackBy: trackByItemId">
  {{ item.name }}
</div>

46. What are Web Workers and how can they be used in an Angular application?

Answer:

Web Workers are a way to run scripts in a background thread, separate from the main execution thread of a web application. This is useful for offloading heavy, long-running computations that would otherwise block the main thread and make the UI unresponsive.

In Angular, you can use the Angular CLI to generate a web worker. You can then post messages to the worker to start a computation, and the worker can post messages back to the main application with the results.

47. What is “tree shaking” and how does it help reduce bundle size?

Answer:

Tree shaking is a process of dead-code elimination. During the build process, the compiler analyzes your code and identifies any parts (like functions, classes, or modules) that are imported but never actually used. It then “shakes” them out of the final bundle, resulting in a smaller application size.

This is one of the key benefits of using ES2015 modules (import/export) and providing services with providedIn: 'root'.

48. What are HTTP Interceptors and how can they be used for performance monitoring?

Answer:

HTTP Interceptors provide a way to intercept incoming and outgoing HTTP requests and responses globally. You can use them for tasks like adding authentication headers, logging, or caching.

For performance monitoring, you could create an interceptor that records the start time of a request and then calculates the duration when the response is received. This information could be logged to the console or sent to a performance monitoring service.

49. How can you debug change detection cycles in Angular?

Answer:

You can use Angular’s built-in debugging utilities. In main.ts, you can enable profiling:

import { enableDebugTools } from '@angular/platform-browser';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';

platformBrowserDynamic().bootstrapModule(AppModule)
  .then(moduleRef => {
    const applicationRef = moduleRef.injector.get(ApplicationRef);
    const componentRef = applicationRef.components[0];
    enableDebugTools(componentRef);
  })
  .catch(err => console.error(err));

Then, in the browser’s developer console, you can access the ng object. For example, ng.profiler.timeChangeDetection() will trigger change detection and log how long it took.

50. What is the role of a Service Worker in an Angular application?

Answer:

A Service Worker is a script that runs in the background in the browser, separate from the web page. It acts as a proxy between the browser and the network.

In Angular, the @angular/pwa package makes it easy to add a service worker. Its primary roles are:

  • Caching: It can cache your application’s assets (the “app shell”) and data. This allows the app to load instantly on subsequent visits, even if the user is offline.
  • Offline Functionality: It enables your application to work offline by serving cached content.
  • Push Notifications: It can receive push notifications from a server.

Testing

51. What is the difference between TestBed.get() and TestBed.inject()?

Answer:

Both are used to get dependencies from the TestBed injector in a unit test.

  • TestBed.get(): The older, non-type-safe way. It is now deprecated.
  • TestBed.inject(): The modern, type-safe way, introduced in Angular 8. It should always be preferred.

52. How do you test a component that has an @Input() property?

Answer:

In your test, after you create the component using TestBed.createComponent(), you can set the input property on the component instance directly. You then need to call fixture.detectChanges() to trigger change detection and update the template with the new input value.

it('should display the user name', () => {
  const fixture = TestBed.createComponent(UserComponent);
  const component = fixture.componentInstance;
  component.user = { name: 'Test User' };
  fixture.detectChanges(); // Trigger change detection
  const compiled = fixture.nativeElement;
  expect(compiled.querySelector('h1').textContent).toContain('Test User');
});

53. How do you test a component that has an @Output() property?

Answer:

You can subscribe to the @Output() property (which is an EventEmitter) on the component instance and check if it emits the expected value when a certain action is performed.

it('should emit the user when the save button is clicked', () => {
  const fixture = TestBed.createComponent(UserFormComponent);
  const component = fixture.componentInstance;
  const testUser = { name: 'Test' };
  component.user = testUser;

  let emittedUser: User;
  component.save.subscribe((user: User) => emittedUser = user);

  const saveButton = fixture.nativeElement.querySelector('button');
  saveButton.click();

  expect(emittedUser).toEqual(testUser);
});

54. What is a “spy” in the context of Jasmine testing, and how would you use it to mock a service?

Answer:

A spy is a feature of the Jasmine testing framework that lets you “spy” on a function. You can track if it was called, how many times it was called, and with which arguments. You can also tell it what to return.

This is extremely useful for mocking dependencies like services. You can create a spy object that mimics the service’s API and provide it in the TestBed configuration.

let userServiceSpy: jasmine.SpyObj<UserService>;

beforeEach(() => {
  const spy = jasmine.createSpyObj('UserService', ['getUsers']);

  TestBed.configureTestingModule({
    declarations: [UsersComponent],
    providers: [{ provide: UserService, useValue: spy }]
  });

  userServiceSpy = TestBed.inject(UserService) as jasmine.SpyObj<UserService>;
});

it('should get users on init', () => {
  const mockUsers = [{ id: 1, name: 'Test' }];
  userServiceSpy.getUsers.and.returnValue(of(mockUsers)); // Return a mock observable

  // ... create component and run test
});

55. What is the purpose of the fakeAsync and tick functions in testing?

Answer:

fakeAsync and tick are used to test asynchronous code in a synchronous way.

  • fakeAsync: You wrap your test function in fakeAsync. This creates a special zone where you can control the passage of time.
  • tick(): Inside a fakeAsync test, you can call tick(milliseconds) to simulate the passage of time. This will cause any pending asynchronous operations (like setTimeout or promises) to be executed.

This makes your tests more robust and faster because you don’t have to use the real async/await or done() callback.

Standalone Components & Modern Angular

56. What are Standalone Components and what are the benefits?

Answer:

Standalone Components (introduced in Angular 14 and made stable in v15) are components, directives, and pipes that do not need to be declared in an NgModule. They manage their own dependencies directly.

Benefits:

  • Simplified API & Reduced Boilerplate: No more NgModule for simple components. This makes the learning curve easier and reduces the amount of code you need to write.
  • Improved Tree-Shaking: Because dependencies are managed at the component level, it can be easier for the build process to eliminate unused code.
  • Easier to Share and Reuse: Standalone components are self-contained, making them easier to share across projects or publish as libraries.

57. How do you provide dependencies or import other components into a Standalone Component?

Answer:

You use the imports array in the @Component decorator. You can import other standalone components, directives, pipes, or even entire NgModules.

import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { HighlightDirective } from '../highlight.directive'; // A standalone directive

@Component({
  standalone: true,
  selector: 'app-user-profile',
  templateUrl: './user-profile.component.html',
  imports: [
    CommonModule,       // For ngIf, ngFor
    MatCardModule,      // Importing an NgModule
    HighlightDirective  // Importing a standalone directive
  ],
})
export class UserProfileComponent { }

58. How do you bootstrap an application using a Standalone Component?

Answer:

Instead of bootstrapping an NgModule, you use the bootstrapApplication function in main.ts and pass it your root standalone component.

// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component'; // A standalone component

bootstrapApplication(AppComponent, {
  providers: [
    // Application-level providers go here
  ]
}).catch(err => console.error(err));

59. What are Angular Signals and how do they differ from RxJS Observables?

Answer:

Angular Signals (introduced in Angular 16) are a new system for reactive state management that enables fine-grained change detection.

  • Signal: A wrapper around a value that can notify interested consumers when that value changes.
  • Computed Signal: A signal whose value is derived from other signals. It’s lazily evaluated and memoized.
  • Effect: An operation that runs whenever one or more signal values change.

Differences from RxJS:

  • Synchronous vs. Asynchronous: Signals are synchronous. When you update a signal, computed signals and effects that depend on it are re-evaluated immediately. RxJS is primarily for managing asynchronous events over time.
  • Pull vs. Push: Signals are “pull”-based. The consumer (like a computed signal or the template) pulls the value when it needs it. Observables are “push”-based; they push values to subscribers.
  • Glitch-Free: The signal dependency graph ensures that you never see an inconsistent state. A signal is only updated once, even if multiple dependencies change at the same time.
  • No zone.js: In the future, signals will enable a zoneless application architecture, which can lead to significant performance improvements.

Signals are excellent for managing synchronous component state, while RxJS remains the tool of choice for handling asynchronous events like HTTP requests and user input events.