Dependency Injection in Angular

By Syncfusion®, Inc. · Last updated

How Angular dependency injection works: tokens, providers, providedIn, the injector hierarchy, root and component-level providers, injection tokens, scope and lifetime.

Introduction to Dependency Injection

A dependency is an object that a class needs to do its work. For example, OrderService needs LoggerService to write log messages.

Dependency Injection (DI) is how a class gets these objects. The class does not create them itself. It asks Angular for them, and Angular gives them to the class.

You already used DI in the previous lesson. When you wrote inject(CartService), you asked Angular for the cart service.

Real-World Example

An order page asks Angular for a NotificationService to message the customer. In India, the app gives it an SmsNotificationService. In Europe, it gives an EmailNotificationService.

The order page code stays the same in both countries.

If a class writes new LoggerService(), it can only use that exact class. With DI, the class only says which service it needs. You can then give it a different class without changing the code of the class.

PointCreate with newDependency injection
Who creates the objectThe class itselfAngular
Share one object between classesHard, because each new creates another objectAngular shares one object for you
Use a fake class in testsYou must edit the classYou change one provider
The dependency needs other servicesYou must create those services tooAngular creates them for you

How Dependency Injection Works

DI has three parts. Learn these three words first, because the rest of the lesson uses them.

Token

A token is the name you use to ask for a dependency. Usually the token is the class itself. In inject(LoggerService), the token is LoggerService.

Provider

A provider is a rule that tells Angular how to create the value for a token. For example: "For the token LoggerService, create a new LoggerService object."

Injector

An injector is the part of Angular that does the work. It stores the providers, creates the objects and keeps them, so it can give the same object again later.

1. Askinject(Logger)2. Look upfind a provider3. Createonly the first time4. Deliversame instanceThe injector keeps the instance, so the next request skips step 3.

How Angular resolves one dependency.

The Steps

When a class calls inject(LoggerService), Angular follows these steps:

  1. The injector looks for a provider for the token LoggerService.
  2. The first time, it creates a LoggerService object with the provider and keeps it.
  3. It returns the object to the class.
  4. On every later request, it returns the same object. It does not create a new one.

If the injector finds no provider, Angular throws a NullInjectorError.

In the next example, the component asks only for PaymentService. But PaymentService needs LoggerService, so Angular creates LoggerService first.

Angular resolves a chain of dependencies

import { Component, Injectable, inject, signal } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class LoggerService {
  readonly lines = signal<string[]>(['LoggerService created']);
  log(message: string) {
    this.lines.update(list => [...list, message]);
  }
}

@Injectable({ providedIn: 'root' })
export class PaymentService {
  private logger = inject(LoggerService);
  constructor() {
    this.logger.log('PaymentService created');
  }
  pay(amount: number) {
    this.logger.log(`Paid $${amount}`);
  }
}

@Component({
  selector: 'app-root',
  template: `
    <button (click)="payments.pay(25)">Pay $25</button>
    @for (line of logger.lines(); track $index) {
      <p>{{ line }}</p>
    }
  `,
})
export class App {
  payments = inject(PaymentService);
  logger = inject(LoggerService);
}

Click Pay $25 several times. A new "Paid" line appears each time, but no new "created" line appears. This shows that Angular reuses the same objects.

Providers

A provider tells the injector what to return for a token. You can add providers in two places:

  • In the providers array of a component. Then the provider works for that component and its child components.
  • In app.config.ts. Then the provider works for the whole app.

A provider can be written in different forms. Each form creates the value in a different way.

Here is one example of each form. They use the same classes as the practice block below.

providers: [
  PriceService,                                             // 1. class name only
  { provide: PriceService, useClass: RupeePriceService },   // 2. useClass
  { provide: StoreConfig, useValue: { storeName: 'Gadget Hub' } }, // 3. useValue
  { provide: Clock, useFactory: () => new Clock(14) },      // 4. useFactory
  { provide: OldLogger, useExisting: LoggerService },       // 5. useExisting
]

How to read a provider object:

  • provide is the token, the name that a class asks for with inject().
  • useClass, useValue, useFactory or useExisting tells Angular what to give for that token.
FormWhat inject() returnsWhen to use it
1. Class name onlyA new object of that classMost cases
2. useClassA new object of a different class: RupeePriceService for PriceServiceChange the class, for example rupee prices instead of dollar prices
3. useValueExactly the value you writeSettings and fixed values
4. useFactoryThe value that the function returnsYou need code to create the value
5. useExistingThe same object as another token: the LoggerService object for OldLoggerOld code asks for OldLogger, and you want it to use LoggerService

The next example uses useClass, useValue and useFactory.

useClass, useValue and useFactory

import { Component, Injectable, inject } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class PriceService {
  label(amount: number) {
    return `$${amount}`;
  }
}

@Injectable()
export class RupeePriceService extends PriceService {
  override label(amount: number) {
    return `₹${amount * 80}`;
  }
}

export class StoreConfig {
  storeName = '';
  freeShippingFrom = 0;
}

@Injectable()
export class Clock {
  hour: number;
  constructor(hour: number) {
    this.hour = hour;
  }
}

@Component({
  selector: 'app-root',
  providers: [
    { provide: PriceService, useClass: RupeePriceService },
    { provide: StoreConfig, useValue: { storeName: 'Gadget Hub', freeShippingFrom: 50 } },
    { provide: Clock, useFactory: () => new Clock(14) },
  ],
  template: `
    <h3>{{ config.storeName }}</h3>
    <p>Mouse: {{ prices.label(15) }}</p>
    <p>Free shipping from {{ prices.label(config.freeShippingFrom) }}</p>
    <p>{{ clock.hour < 12 ? 'Good morning' : 'Good afternoon' }}</p>
  `,
})
export class App {
  prices = inject(PriceService);
  config = inject(StoreConfig);
  clock = inject(Clock);
}

What happens in this example:

  • The component asks for PriceService. The useClass provider gives it a RupeePriceService, so prices are shown in rupees.
  • The useValue provider gives the store settings object.
  • The useFactory provider calls a function that creates a Clock with the hour 14.

Delete the PriceService line in providers and click Run. The prices are now shown in dollars.

providedIn

providedIn is an option of @Injectable(). It adds a provider for the service, so you do not need a providers array.

It can have these values:

  • 'root': Angular creates one object for the whole app. Use this in most cases.
  • 'platform': one object is shared by several Angular apps on the same page. This is rarely used.
  • No providedIn: if you write only @Injectable(), Angular has no provider for the service. You must add it to a providers array. Otherwise, inject() throws an error.

Use providedIn: 'root' by default. Use @Injectable() without providedIn only when each component needs its own object of the service. The section "Component-Level Providers" shows this.

Provider Hierarchy

A hierarchy is a structure with levels, like a family tree. Angular has many injectors, and they are arranged in levels that follow the component tree.

The Levels

  • Root injector: the top level. It has the providedIn: 'root' services and the providers in app.config.ts.
  • Component injector: each component that has a providers array gets its own injector.

How Angular Searches

When a class calls inject(), Angular first looks in the injector of the nearest component. If there is no provider there, it looks in the parent component, then the next parent, and so on up to the root. Angular uses the first provider it finds.

Root injectorprovidedIn: 'root' servicesApp injectorTHEME = 'light'Sidebar injectorTHEME = 'dark'Panel injectorno providersPanel asks for THEMEFound here: 'dark'Angular starts at the component and moves up. It uses the first provider it finds.

The injector hierarchy of the next example. The nearest provider wins.

In the next example, App provides the theme 'light' and Sidebar provides the theme 'dark'.

The nearest provider wins

import { Component, InjectionToken, inject } from '@angular/core';

export const THEME = new InjectionToken<string>('THEME');

@Component({
  selector: 'app-panel',
  template: `<p>{{ name }} uses the {{ theme }} theme</p>`,
})
export class Panel {
  name = 'Panel';
  theme = inject(THEME);
}

@Component({
  selector: 'app-sidebar',
  providers: [{ provide: THEME, useValue: 'dark' }],
  imports: [Panel],
  template: `<p>Sidebar:</p><app-panel />`,
})
export class Sidebar {}

@Component({
  selector: 'app-root',
  providers: [{ provide: THEME, useValue: 'light' }],
  imports: [Panel, Sidebar],
  template: `<app-panel /><app-sidebar />`,
})
export class App {}

The first panel is inside App only, so Angular finds the 'light' provider in App. The second panel is inside Sidebar, so Angular finds the 'dark' provider in Sidebar first and stops searching.

Root-Level Providers

A root-level provider creates one object that the whole app shares. You can add it in two ways:

// Way 1: in the service (recommended)
@Injectable({ providedIn: 'root' })
export class AuthService {}

// Way 2: in app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [AuthService],
};
  • Use it for: the signed-in user, the cart, API services, logging and app settings.
  • Benefit: every component reads the same data.
  • Limitation: the object stays in memory until the user closes the app. So you must clear private data yourself, for example when the user logs out.

Component-Level Providers

A component-level provider is a service in the providers array of a component. Angular creates a new service object for each copy of that component on the screen. The child components of that component use the same object.

In the next example, the page shows three Counter components. Each one has providers: [CounterService].

Each counter gets its own service

import { Component, Injectable, inject, signal } from '@angular/core';

@Injectable()
export class CounterService {
  readonly count = signal(0);
  increment() {
    this.count.update(n => n + 1);
  }
}

@Component({
  selector: 'app-counter',
  providers: [CounterService],
  template: `
    <p>
      {{ label }}: {{ counter.count() }}
      <button (click)="counter.increment()">+1</button>
    </p>
  `,
})
export class Counter {
  label = 'Counter';
  counter = inject(CounterService);
}

@Component({
  selector: 'app-root',
  imports: [Counter],
  template: `<app-counter /><app-counter /><app-counter />`,
})
export class App {}

Each +1 button changes only its own counter, because each counter has its own CounterService object. If CounterService used providedIn: 'root' and there were no providers array, all three counters would show the same number.

PointRoot-levelComponent-level
Where you add itprovidedIn: 'root' or app.config.tsThe component's providers array
Number of objectsOne for the whole appOne for each copy of the component
When Angular creates itThe first time a class asks for itWhen Angular creates the component
When Angular removes itWhen the app closesWhen Angular removes the component
Example usesCart, signed-in user, API serviceData of one form, one editor or one step of a wizard

Injection Tokens

A class can be a token. But some values are not classes, for example an API address (a string), a page size (a number) or a settings object. For these values, you create an InjectionToken.

export const API_URL = new InjectionToken<string>('API_URL');

How to use it:

  • The text 'API_URL' is a description. Angular shows it in error messages.
  • To give a value, add a provider: { provide: API_URL, useValue: 'https://api.shop.com' }.
  • To read the value, call inject(API_URL).
  • You can also give the token a default value with a factory function. Then it works without a provider.

Configuration with an InjectionToken

import { Component, InjectionToken, inject } from '@angular/core';

export const API_URL = new InjectionToken<string>('API_URL', {
  providedIn: 'root',
  factory: () => 'https://api.shop.com',
});

export const PAGE_SIZE = new InjectionToken<number>('PAGE_SIZE');

@Component({
  selector: 'app-root',
  providers: [{ provide: PAGE_SIZE, useValue: 20 }],
  template: `
    <p>Products endpoint: {{ apiUrl }}/products</p>
    <p>Page size: {{ pageSize }}</p>
  `,
})
export class App {
  apiUrl = inject(API_URL);
  pageSize = inject(PAGE_SIZE);
}

API_URL has a default value from its factory function. PAGE_SIZE has no default value, so the component gives it the value 20 in providers.

Do not use a plain string such as 'API_URL' as a token. Two libraries can use the same string, and then one value replaces the other. Each InjectionToken object is different from every other object, so this cannot happen.

Service Scope

The scope of a service is the part of the app that can use one object of the service. The place where you add the provider decides the scope.

  • App scope: with providedIn: 'root', the whole app uses one object.
  • Component scope: with providers: [...] in a component, that component and its child components use one object. Other parts of the app cannot use it.

Optional Dependencies

If no provider exists in the scope, inject() throws an error. Write inject(Service, { optional: true }) to get null instead of an error.

Service Lifetime

The lifetime of a service is the time between Angular creating the object and Angular removing it.

  • Root service: Angular creates it the first time a class asks for it. It stays until the user closes the app.
  • Component-level service: Angular creates it together with its component and removes it together with the component. Before removing it, Angular calls the service's ngOnDestroy() method. Write code there to stop timers.

In the next example, DraftService is a component-level service of Editor. Click Toggle editor to remove the editor, and click again to create it.

A component-level service is destroyed with its component

import { Component, Injectable, OnDestroy, inject, signal } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class AuditLog {
  readonly lines = signal<string[]>([]);
  add(line: string) {
    this.lines.update(list => [...list, line]);
  }
}

@Injectable()
export class DraftService implements OnDestroy {
  private log = inject(AuditLog);
  constructor() {
    this.log.add('DraftService created');
  }
  ngOnDestroy() {
    this.log.add('DraftService destroyed');
  }
}

@Component({
  selector: 'app-editor',
  providers: [DraftService],
  template: `<p>Editor is open</p>`,
})
export class Editor {
  draft = inject(DraftService);
}

@Component({
  selector: 'app-root',
  imports: [Editor],
  template: `
    <button (click)="open.set(!open())">Toggle editor</button>
    @if (open()) {
      <app-editor />
    }
    @for (line of log.lines(); track $index) {
      <p>{{ line }}</p>
    }
  `,
})
export class App {
  open = signal(true);
  log = inject(AuditLog);
}

AuditLog is a root service, so it stays and keeps all the messages. DraftService is created each time the editor appears and removed each time the editor disappears.

Common Mistakes

MistakeWhat you seeFix
@Injectable() without providedIn and without a providers entryNullInjectorError: No provider for the serviceAdd providedIn: 'root', or add the service to providers
Adding a root service to a component's providers tooThe component gets its own new object, so the data is not sharedRemove the service from the component's providers
Using a string as a tokenTwo values with the same name replace each otherUse new InjectionToken<T>('...')
Keeping important data in a component-level serviceThe data is lost when the component is removedUse a root service for data that must stay
Two services that inject each otherError: circular dependencyMove the shared code into a third service

Check Yourself

Exercises

What to Remember

  • With DI, a class asks for its dependencies with inject(), and Angular gives them. A provider tells Angular how to create a value. The injector creates it once and gives the same object again.
  • Angular searches for a provider from the nearest component up to the root. A root provider gives one object for the app. A component provider gives one object for each copy of the component, and Angular removes it with the component.
  • Use useClass, useValue and useFactory to choose what a token returns. Use InjectionToken for values that are not classes.

Next: Routing, how to give each screen its own URL and move between screens.