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.
| Point | Create with new | Dependency injection |
|---|---|---|
| Who creates the object | The class itself | Angular |
| Share one object between classes | Hard, because each new creates another object | Angular shares one object for you |
| Use a fake class in tests | You must edit the class | You change one provider |
| The dependency needs other services | You must create those services too | Angular 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:
- The injector looks for a provider for the token
LoggerService. - The first time, it creates a
LoggerServiceobject with the provider and keeps it. - It returns the object to the class.
- 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
providersarray 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:
provideis the token, the name that a class asks for withinject().useClass,useValue,useFactoryoruseExistingtells Angular what to give for that token.
| Form | What inject() returns | When to use it |
|---|---|---|
| 1. Class name only | A new object of that class | Most cases |
2. useClass | A new object of a different class: RupeePriceService for PriceService | Change the class, for example rupee prices instead of dollar prices |
3. useValue | Exactly the value you write | Settings and fixed values |
4. useFactory | The value that the function returns | You need code to create the value |
5. useExisting | The same object as another token: the LoggerService object for OldLogger | Old 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. TheuseClassprovider gives it aRupeePriceService, so prices are shown in rupees. - The
useValueprovider gives the store settings object. - The
useFactoryprovider calls a function that creates aClockwith 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 aprovidersarray. 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 inapp.config.ts. - Component injector: each component that has a
providersarray 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.
| Point | Root-level | Component-level |
|---|---|---|
| Where you add it | providedIn: 'root' or app.config.ts | The component's providers array |
| Number of objects | One for the whole app | One for each copy of the component |
| When Angular creates it | The first time a class asks for it | When Angular creates the component |
| When Angular removes it | When the app closes | When Angular removes the component |
| Example uses | Cart, signed-in user, API service | Data 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
factoryfunction. 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
| Mistake | What you see | Fix |
|---|---|---|
@Injectable() without providedIn and without a providers entry | NullInjectorError: No provider for the service | Add providedIn: 'root', or add the service to providers |
Adding a root service to a component's providers too | The component gets its own new object, so the data is not shared | Remove the service from the component's providers |
| Using a string as a token | Two values with the same name replace each other | Use new InjectionToken<T>('...') |
| Keeping important data in a component-level service | The data is lost when the component is removed | Use a root service for data that must stay |
| Two services that inject each other | Error: circular dependency | Move 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,useValueanduseFactoryto choose what a token returns. UseInjectionTokenfor values that are not classes.
Next: Routing, how to give each screen its own URL and move between screens.