Skip to content
Angular ng services 4 min read

Providers & Injection Scopes

A provider tells Angular’s dependency-injection system how to create a value for a token, and where you register that provider decides how long the resulting instance lives and who gets to share it. Registering a service at the application root gives you a single shared singleton; registering it on a component or a route gives you a fresh, scoped instance tied to that part of the UI. Choosing the right scope is the difference between accidentally leaking state across your whole app and cleanly isolating it.

How providers map to scopes

Every Angular application is built from a tree of injectors. When a class asks for a dependency via inject() or a constructor parameter, Angular walks up that tree until it finds a provider for the requested token. The injector that owns the provider is the one that creates and caches the instance — so the location of the provider literally defines the scope.

Registration siteInjectorInstances createdTypical use
providedIn: 'root'Root environment injectorOne (singleton)App-wide state, HTTP services, caches
Route providersRoute environment injectorOne per activated route subtreeFeature-scoped state
Component providersElement injectorOne per component instancePer-widget state, isolated config
providedIn: 'platform'Platform injectorOne per platformMulti-app shared services (rare)

Root-level providers

The default and most common choice is providedIn: 'root'. The service becomes a singleton in the root injector, is available everywhere without any extra wiring, and is fully tree-shakable — if nothing injects it, the bundler drops it.

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

@Injectable({ providedIn: 'root' })
export class CartService {
  private readonly items = signal<string[]>([]);
  readonly count = () => this.items().length;

  add(sku: string): void {
    this.items.update((list) => [...list, sku]);
  }
}

Any component, guard, or other service can now inject it and they all see the same instance:

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

@Component({
  selector: 'app-add-button',
  standalone: true,
  template: `<button (click)="cart.add('SKU-1')">Add ({{ cart.count() }})</button>`,
})
export class AddButtonComponent {
  protected readonly cart = inject(CartService);
}

Component-level providers

Listing a service in a component’s providers array creates a new instance for each instance of that component, stored in the component’s element injector. Child components nested in its template share that instance, but sibling instances of the same component each get their own. This is the right tool for per-widget state that must not bleed between instances.

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

@Injectable()
export class PanelState {
  readonly open = signal(false);
  toggle() { this.open.update((v) => !v); }
}

@Component({
  selector: 'app-panel',
  standalone: true,
  providers: [PanelState],
  template: `
    <button (click)="state.toggle()">Toggle</button>
    @if (state.open()) {
      <section><ng-content /></section>
    }
  `,
})
export class PanelComponent {
  protected readonly state = inject(PanelState);
}

Notice the service is decorated with a bare @Injectable() (no providedIn) because its lifetime is owned by the component, not the root injector. Render two panels and toggling one leaves the other untouched — each owns a private PanelState.

Use a viewProviders array instead of providers when you want the service visible to the component’s own template but not to content projected in via <ng-content>. It is a tighter scope.

Route-level providers

Lazy-loaded and even eagerly-loaded routes can declare a providers array. Angular creates an environment injector for that route’s subtree, so the service is a singleton for that feature and is destroyed when you navigate away. This gives you feature isolation without polluting the root injector.

import { Routes } from '@angular/router';
import { ReportStore } from './report.store';

export const routes: Routes = [
  {
    path: 'reports',
    providers: [ReportStore],
    loadComponent: () => import('./reports.component').then((m) => m.ReportsComponent),
    children: [
      { path: ':id', loadComponent: () => import('./report-detail.component').then((m) => m.ReportDetailComponent) },
    ],
  },
];

Both ReportsComponent and its child ReportDetailComponent inject the same ReportStore, but the store ceases to exist once the user leaves /reports, so cached report data is discarded automatically.

Verifying the scope

A quick way to prove instances are (or are not) shared is to log a unique id in the constructor:

@Injectable()
export class PanelState {
  readonly id = Math.random().toString(36).slice(2, 7);
  constructor() { console.log('PanelState created:', this.id); }
}

Rendering two <app-panel> elements prints:

Output:

PanelState created: a3f9k
PanelState created: q7m2p

Two distinct ids confirm two separate instances. Switch the registration to providedIn: 'root' and you would see the constructor log exactly once for the whole application.

Best practices

  • Prefer providedIn: 'root' by default — it is tree-shakable and avoids accidental duplicate instances.
  • Reach for component providers only when each component instance genuinely needs isolated state.
  • Use route-level providers to scope feature state to a lazy route so it is created and destroyed with the feature.
  • Drop providedIn from services that are meant to be scoped, so they cannot leak into the root injector by mistake.
  • Use viewProviders instead of providers when projected content should not see the service.
  • Avoid registering the same service in both root and a component — the closer provider silently wins and creates a second instance.
Last updated June 14, 2026
Was this helpful?