Suggest an editImprove this articleRefine the answer for “Injector hierarchy in Angular”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Injector hierarchy in Angular** is a tree of `Injector` instances mirroring the component tree. When resolving a token, Angular starts at the closest injector and walks up until it finds a provider or throws `NullInjectorError`. ```ts @Injectable({ providedIn: 'root' }) // One singleton for the whole app export class AuthService {} @Component({ providers: [AuthService] // New instance per component, shadows root }) export class ChildComponent {} ``` **Key point:** child injectors inherit from parents but override them when they declare the same token locally.Shown above the full answer for quick recall.Answer (EN)Image**Injector hierarchy in Angular** is a tree of `Injector` instances that mirrors the component tree. When Angular resolves a dependency, it starts at the closest injector and walks up until it finds a provider or throws an error. ## Theory ### TL;DR - Think of it like a company org chart: ask your direct manager first (local injector), then the department head (module), then the CEO (root) if needed - Child injectors inherit from parents but shadow them when they declare their own provider for the same token - `providedIn: 'root'` creates one singleton for the whole app; `Component.providers` creates a new instance per component - Lazy-loaded modules get their own injector, so a service in `NgModule.providers` of a lazy module is NOT shared with root - Decision rule: shared state across the app = root, feature isolation = module, per-instance data = component ### Quick example ```ts // Root injector has UserService (providedIn: 'root') @Injectable({ providedIn: 'root' }) export class UserService { name = 'real user'; } // ChildComponent declares its own provider - shadows root @Component({ selector: 'app-child', template: '...', providers: [{ provide: UserService, useClass: MockUserService }] }) export class ChildComponent { constructor(private user: UserService) { console.log(user instanceof MockUserService); // true - local injector wins } } ``` The child gets `MockUserService` even though root has `UserService`. Siblings of `ChildComponent` still get the root instance. ### How Angular resolves dependencies When a component or service requests a token, Angular walks up the injector tree: 1. Check the component's own injector (declared in `providers[]`) 2. If not found, check the parent component's injector 3. Continue up through ancestor components 4. Reach the module injector 5. Reach the root (application) injector 6. If nothing found, throw `NullInjectorError` Each `Injector` instance stores resolved services in a `records` map. So the same token is not instantiated twice within the same injector. That is the singleton guarantee at each level. ### The four injector levels **Root injector.** `@Injectable({ providedIn: 'root' })` registers the service with the application-level injector. One instance for the entire app, created lazily on first request. Tree-shakable: if nothing injects the service, it is not included in the bundle. **Module injector.** Listing a service in `@NgModule.providers` creates an instance scoped to that module's injector. For eagerly loaded modules, this ends up merged with root. For lazy-loaded modules, it creates a child injector, so the service is separate from root. **Component injector.** Adding a service to `@Component.providers` creates a new instance for each component instance. That instance is destroyed when the component is destroyed. Children inherit it unless they override it. **Element injector (directives).** `@Directive.providers` works the same way as component providers, scoped to the element where the directive is applied. ### When to use each level - App-wide shared state (auth, logging, feature flags): `providedIn: 'root'` - Feature isolation in a lazy-loaded route: `NgModule.providers` inside the lazy module - Per-component instance data (local form state, per-card cache): `Component.providers` - Mocking a service in a subtree for testing or e2e: component or module-level provider with `useClass` - Library services that should not pollute the global namespace: `providedIn: 'platform'` or module-level ### How the Angular runtime builds this At bootstrap, Angular recursively creates `Injector` instances for each `@Component` with a `providers[]` array and links each one to its parent via the `parent` property. During resolution it calls `getAt()`, traversing upward from the request site. Results are cached in the `records` map per injector, so the second request for the same token returns the cached instance without re-instantiation. In Angular 14+, `providedIn: 'root'` is statically analyzed, making it tree-shakable. Services listed in `providers[]` arrays are not tree-shaken because they are referenced explicitly in module or component metadata. Standalone components (Angular 14+) partially flatten this: they use an `EnvironmentInjector` instead of `NgModule` injectors, but component-level providers still create element injectors the same way. ### Common mistakes **Mistake 1: expecting a lazy module provider to share the root instance** ```ts // Wrong: this creates a SEPARATE instance, not the root singleton @NgModule({ providers: [DataService] // Lazy module gets its own injector! }) export class FeatureModule {} ``` If `DataService` is also `providedIn: 'root'`, the lazy module provider shadows it for everything inside the module. Components outside still get the root singleton. To share root: remove from `NgModule.providers` and rely on `providedIn: 'root'` alone. **Mistake 2: providing in both module and component** ```ts @NgModule({ providers: [CartService] }) // ...and also: @Component({ providers: [CartService] }) export class CartComponent {} ``` The component injector shadows the module injector. You get two separate instances consuming extra memory. Angular DevTools makes this visible: open the component tree and check injector sources per component. **Mistake 3: forgetting that child components inherit parent component providers** ```ts @Component({ providers: [SharedService] }) export class ParentComponent {} // ChildComponent gets ParentComponent's SharedService instance // NOT root's, NOT a fresh one @Component({ selector: 'app-child', template: '' }) export class ChildComponent { constructor(private s: SharedService) {} } ``` I have seen this bite teams when they add a provider to a parent component "just for this parent" and then wonder why the child gets unexpected state. The child inherits it automatically. **Mistake 4: using `providedIn: 'root'` in a published library** This registers the service in the consuming app's root injector at build time. That is fine for apps but can conflict when multiple library consumers expect different instances. Use `providedIn: 'platform'` for microfrontend-shared services, or let the consuming app decide. **Mistake 5: confusing `viewProviders` with `providers`** `viewProviders` restricts the service to the component's own view, excluding content projected via `<ng-content>`. `providers` shares with both view and content children. Using the wrong one causes a `NullInjectorError` in projected components that try to inject the service. ### Real-world usage - Angular Material: `OverlayRef` and dialog services use `providedIn: 'root'` for the overlay container singleton - NgRx: `StoreModule.forRoot()` registers state at root; `StoreModule.forFeature()` adds feature state in a child module injector - Nx workspaces: lazy feature libraries use module-level providers to keep services isolated from the shell app - Angular Universal (SSR): platform-level providers swap browser APIs for Node-compatible ones without touching root ### Follow-up questions **Q:** What happens when a lazy-loaded module provides a service that is also `providedIn: 'root'`? **A:** The lazy module's injector shadows root for any component inside that module. Components outside the lazy module still get the root singleton. Two separate instances exist simultaneously in the same running app. **Q:** How does `@SkipSelf()` change resolution? **A:** It skips the current component's injector and starts lookup from the parent. Useful when a service needs to access its parent's version of itself, for example a recursive tree node component that needs its parent's `TreeService` instead of its own. **Q:** What does `@Self()` do? **A:** It restricts lookup to the current component's injector only. If the token is not found there, Angular throws immediately without walking up the tree. Used to enforce that a dependency must be provided at the same level as the consumer. **Q:** How do standalone components change the injector hierarchy? **A:** Standalone components (Angular 14+) use `EnvironmentInjector` instead of `NgModule` injectors. `bootstrapApplication()` creates the root `EnvironmentInjector`. Component-level `providers` still create element injectors. The `inject()` function used inside signals and computed values hooks into this same tree. **Q (senior):** A directive is applied to a component and injects a service. Where does Angular look for that service? **A:** It starts at the element injector of the element the directive is on, which includes providers from both the component and the directive. Then it walks up the component tree normally. The directive does not get a separate injector branch; it shares the element's injector context with the host component. ## Examples ### Basic: scoped instance per component ```ts // counter.service.ts @Injectable() export class CounterService { count = 0; increment() { this.count++; } } // card.component.ts @Component({ selector: 'app-card', template: ` <button (click)="counter.increment()">+</button> <span>{{ counter.count }}</span> `, providers: [CounterService] // Each card gets its own counter instance }) export class CardComponent { constructor(public counter: CounterService) {} } ``` Two `<app-card>` elements on the same page show independent counters. If `CounterService` were `providedIn: 'root'`, they would share state and both buttons would increment the same number. ### Intermediate: mocking a service in a lazy-loaded feature ```ts // auth.service.ts @Injectable({ providedIn: 'root' }) export class AuthService { isLoggedIn = false; login() { this.isLoggedIn = true; } } // mock-auth.service.ts @Injectable() export class MockAuthService extends AuthService { isLoggedIn = true; // Always logged in for e2e test module } // auth-test.module.ts (lazy loaded) @NgModule({ providers: [{ provide: AuthService, useClass: MockAuthService }] }) export class AuthTestModule {} // dashboard.component.ts (inside AuthTestModule) @Component({ template: `<span *ngIf="auth.isLoggedIn">Welcome</span>` }) export class DashboardComponent { constructor(public auth: AuthService) {} // Gets MockAuthService - always renders "Welcome" } ``` Root's `AuthService` is untouched. Only components inside `AuthTestModule` see the mock. Everything outside the lazy boundary continues using the real singleton. ### Advanced: `@SkipSelf()` for self-referential services ```ts // logger.service.ts @Injectable() export class LoggerService { constructor( @Optional() @SkipSelf() private parent: LoggerService ) {} log(msg: string) { const prefix = this.parent ? '[child]' : '[root]'; console.log(`${prefix} ${msg}`); } } // parent.component.ts @Component({ selector: 'app-parent', template: '<app-child></app-child>', providers: [LoggerService] // Root logger instance }) export class ParentComponent { constructor(private logger: LoggerService) { this.logger.log('parent init'); // "[root] parent init" } } // child.component.ts @Component({ selector: 'app-child', template: '', providers: [LoggerService] // Child logger, aware of its parent }) export class ChildComponent { constructor(private logger: LoggerService) { this.logger.log('child init'); // "[child] child init" } } ``` `@SkipSelf()` tells Angular to look for `LoggerService` starting from the parent, not the current component. Combined with `@Optional()`, this avoids a circular dependency error at the root level where there is no parent logger to find.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.