Skip to main content

Angular Hierarchical Injectors

Introduction

Angular's dependency injection (DI) system is one of its most powerful features, allowing you to create reusable, maintainable, and testable applications. At the heart of this system is the concept of hierarchical injectors.

In this tutorial, you'll learn:

  • What hierarchical injectors are
  • How Angular's injector tree works
  • How to configure providers at different levels
  • How to leverage this system for better application design

Whether you're building a simple application or a complex enterprise system, understanding how Angular resolves dependencies through its hierarchical injector system is essential knowledge.

What Are Hierarchical Injectors?

In Angular, dependencies are provided through a system of injectors organized in a tree structure that parallels your application's component tree. Each component in your application has its own injector, and these injectors form a hierarchy.

The key concept is this: When Angular needs to resolve a dependency, it starts at the component's own injector and works its way up the injector tree until it finds a provider for that dependency.

This hierarchical structure allows you to:

  1. Override providers at different levels of your application
  2. Scope services to specific components
  3. Share instances of services where needed
  4. Create isolated instances where appropriate

The Injector Tree Structure

Angular's injector hierarchy follows this general structure (from top to bottom):

  1. Platform injector - For platform-specific dependencies
  2. Root injector - Created for the AppModule
  3. Router injectors - Created for lazy-loaded modules
  4. Component injectors - Created for each component

Let's visualize this with a diagram:

Platform Injector

Root Injector (AppModule)

Router Injectors (Lazy-loaded modules)

Component Injectors

Configuring Providers at Different Levels

Let's look at how to configure providers at different levels of the injector hierarchy.

Root-level Providers

When you provide a service at the root level, the same instance is shared across your entire application.

typescript
// data.service.ts
import { Injectable } from '@angular/core';

@Injectable({
providedIn: 'root' // This makes the service available app-wide
})
export class DataService {
private data = [];

addItem(item: string) {
this.data.push(item);
}

getItems() {
return this.data;
}
}

Module-level Providers

You can provide services at the module level, making them available to all components within that module.

typescript
// feature.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FeatureService } from './feature.service';

@NgModule({
imports: [CommonModule],
declarations: [...],
providers: [FeatureService] // Available to all components in this module
})
export class FeatureModule { }

Component-level Providers

When you provide a service at the component level, a new instance is created for that component and all its child components.

typescript
// parent.component.ts
import { Component } from '@angular/core';
import { CounterService } from './counter.service';

@Component({
selector: 'app-parent',
templateUrl: './parent.component.html',
providers: [CounterService] // New instance for this component and its children
})
export class ParentComponent {
constructor(private counterService: CounterService) {}

increment() {
this.counterService.increment();
}

get count() {
return this.counterService.count;
}
}

How Dependency Resolution Works

When Angular needs to inject a service into a component, it follows these steps:

  1. Check the component's own injector for the requested service
  2. If not found, check the injector of the parent component
  3. Continue up the component tree until reaching the root injector
  4. If still not found, check module injectors
  5. Finally, check the root injector
  6. If the service isn't found in any injector, Angular throws an error

Let's see this in action with a practical example.

Practical Example: Managing Component State

Consider a scenario where we have a shopping cart application with different components that need to manage state differently:

typescript
// cart.service.ts
import { Injectable } from '@angular/core';

@Injectable()
export class CartService {
private items = [];

addToCart(product) {
this.items.push(product);
}

getItems() {
return this.items;
}

clearCart() {
this.items = [];
return this.items;
}
}

Now, let's create a component structure that showcases hierarchical injection:

typescript
// app.component.ts
import { Component } from '@angular/core';
import { CartService } from './cart.service';

@Component({
selector: 'app-root',
template: `
<h1>My Store</h1>
<app-product-list></app-product-list>
<app-cart></app-cart>
`,
providers: [CartService] // Provided at root component level
})
export class AppComponent {}
typescript
// product-list.component.ts
import { Component } from '@angular/core';
import { CartService } from './cart.service';

@Component({
selector: 'app-product-list',
template: `
<h2>Products</h2>
<div *ngFor="let product of products">
{{ product.name }} - ${{ product.price }}
<button (click)="addToCart(product)">Add to cart</button>
</div>
`
})
export class ProductListComponent {
products = [
{ id: 1, name: 'Phone', price: 799 },
{ id: 2, name: 'Laptop', price: 1299 },
{ id: 3, name: 'Headphones', price: 99 }
];

constructor(private cartService: CartService) {}

addToCart(product) {
this.cartService.addToCart(product);
console.log('Product added to cart!');
}
}
typescript
// cart.component.ts
import { Component } from '@angular/core';
import { CartService } from './cart.service';

@Component({
selector: 'app-cart',
template: `
<h2>Cart</h2>
<div *ngIf="items.length === 0">Your cart is empty</div>
<div *ngFor="let item of items">
{{ item.name }} - ${{ item.price }}
</div>
<p>Total: ${{ total }}</p>
<button (click)="clearCart()">Clear Cart</button>
`
})
export class CartComponent {
items = [];

constructor(private cartService: CartService) {
this.items = this.cartService.getItems();
}

clearCart() {
this.items = this.cartService.clearCart();
}

get total() {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}

In this example, because CartService is provided at the AppComponent level, both ProductListComponent and CartComponent share the same instance. When a product is added to the cart in ProductListComponent, it's visible in the CartComponent.

Using Multiple Service Instances with @Host and @Self

Sometimes you want to limit how far up the injector tree Angular looks for dependencies. Angular provides several decorators to control this behavior:

  • @Self(): Only look for the dependency in the component's own injector
  • @SkipSelf(): Skip the component's own injector and start looking from the parent
  • @Host(): Only look until the host component
  • @Optional(): Return null if the dependency isn't found (instead of throwing an error)

Let's see how to use these decorators:

typescript
// logger.service.ts
import { Injectable } from '@angular/core';

@Injectable()
export class LoggerService {
private logs: string[] = [];

log(message: string) {
this.logs.push(message);
console.log(message);
}

getLogs() {
return this.logs;
}
}
typescript
// child.component.ts
import { Component, Host, Optional } from '@angular/core';
import { LoggerService } from './logger.service';

@Component({
selector: 'app-child',
template: `<p>Child Component</p>`,
})
export class ChildComponent {
constructor(
@Host() @Optional() private logger: LoggerService
) {
if (logger) {
logger.log('ChildComponent initialized');
} else {
console.log('No logger available');
}
}
}

Real-world Application: Feature Module with Isolated Services

A common use case for hierarchical injectors is to create isolated feature modules with their own service instances.

Consider a dashboard application with multiple widgets, where each widget needs its own state management:

typescript
// widget.service.ts
import { Injectable } from '@angular/core';

@Injectable()
export class WidgetService {
private data: any;

setData(data: any) {
this.data = data;
}

getData() {
return this.data;
}

refreshData() {
console.log('Refreshing widget data...');
// Implementation to refresh data
}
}
typescript
// weather-widget.component.ts
import { Component, OnInit } from '@angular/core';
import { WidgetService } from './widget.service';

@Component({
selector: 'app-weather-widget',
template: `
<div class="widget">
<h3>Weather Widget</h3>
<div *ngIf="weatherData">
<p>Temperature: {{ weatherData.temperature }}°C</p>
<p>Condition: {{ weatherData.condition }}</p>
</div>
<button (click)="refresh()">Refresh</button>
</div>
`,
providers: [WidgetService] // Each widget gets its own service instance
})
export class WeatherWidgetComponent implements OnInit {
weatherData: any;

constructor(private widgetService: WidgetService) {}

ngOnInit() {
// In a real app, this would come from an API
const data = { temperature: 22, condition: 'Sunny' };
this.widgetService.setData(data);
this.weatherData = this.widgetService.getData();
}

refresh() {
this.widgetService.refreshData();
// Update data after refresh
this.weatherData = { temperature: 23, condition: 'Partly Cloudy' };
}
}
typescript
// stock-widget.component.ts
import { Component, OnInit } from '@angular/core';
import { WidgetService } from './widget.service';

@Component({
selector: 'app-stock-widget',
template: `
<div class="widget">
<h3>Stock Widget</h3>
<div *ngIf="stockData">
<p>Symbol: {{ stockData.symbol }}</p>
<p>Price: ${{ stockData.price }}</p>
<p>Change: {{ stockData.change }}%</p>
</div>
<button (click)="refresh()">Refresh</button>
</div>
`,
providers: [WidgetService] // Each widget gets its own service instance
})
export class StockWidgetComponent implements OnInit {
stockData: any;

constructor(private widgetService: WidgetService) {}

ngOnInit() {
// In a real app, this would come from an API
const data = { symbol: 'AAPL', price: 150.25, change: 0.75 };
this.widgetService.setData(data);
this.stockData = this.widgetService.getData();
}

refresh() {
this.widgetService.refreshData();
// Update data after refresh
this.stockData = { symbol: 'AAPL', price: 151.50, change: 1.25 };
}
}
typescript
// dashboard.component.ts
import { Component } from '@angular/core';

@Component({
selector: 'app-dashboard',
template: `
<h2>Dashboard</h2>
<div class="widgets-container">
<app-weather-widget></app-weather-widget>
<app-stock-widget></app-stock-widget>
</div>
`
})
export class DashboardComponent {}

In this example:

  1. Each widget component provides its own instance of WidgetService
  2. The weather widget and stock widget maintain their own separate states
  3. When one widget refreshes, it doesn't affect the other widget

This pattern is extremely useful for complex applications with multiple independent components that need their own state management.

Summary

Angular's hierarchical injector system provides a powerful way to manage dependencies in your application. Key takeaways include:

  • Angular resolves dependencies by traversing up the injector tree
  • Services can be provided at root, module, or component level
  • Root-level services (providedIn: 'root') create singleton instances shared across the application
  • Component-level services create new instances for that component and its children
  • Special decorators like @Self, @Host, and @SkipSelf give you fine-grained control over dependency resolution

Understanding these concepts allows you to:

  • Share services where appropriate
  • Isolate services where needed
  • Override services at different levels of your application
  • Create more maintainable, testable, and scalable Angular applications

Additional Resources

Exercises

  1. Create a parent component with a counter service provided at the component level, and see how child components get their own instance.
  2. Implement a theme service that's provided at the root level, allowing all components to change the application theme.
  3. Create a feature module with lazy loading that provides its own service instances.
  4. Experiment with @Self and @SkipSelf decorators to see how they affect dependency resolution.
  5. Build a dashboard with multiple widgets, each with isolated state management using component-level injectors.

By mastering hierarchical injectors, you'll be able to design more flexible and maintainable Angular applications!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)