Angular Dependency Injection
โฑ๏ธ ~5-minute bite ยท solve the sandbox to master
5-Year-Old Metaphor
โ The physical, real-world picture. No jargon.๐ช DI is a city-wide supply chain where the nearest warehouse ships first. If your local corner store has milk (component provider), you buy there. If not, go to the regional hub (parent), then the city warehouse (root). Root always has stock because it serves everyone.
๐๏ธ
Root injector
providedIn: 'root' โ one singleton for the whole app. Tree-shakeable.
๐
Component injector
providers: [] in @Component โ new instance per component. Destroyed with component.
๐ช
Token-based
InjectionToken<T> โ typed tokens for non-class values: config, arrays, primitives.
inject() โ the modern alternative to constructor injection
| 1 | // Constructor injection (classic) |
| 2 | constructor(private auth: AuthService, private router: Router) {} |
| 3 | ย |
| 4 | // inject() โ works anywhere in injection context (Angular 14+) |
| 5 | export class MyComponent { |
| 6 | private auth = inject(AuthService); |
| 7 | private router = inject(Router); |
| 8 | private config = inject(APP_CONFIG); // InjectionToken |
| 9 | private opt = inject(SomeService, { optional: true }); // optional |
| 10 | } |
| 11 | ย |
| 12 | // Also works in standalone functions (great for composable hooks) |
| 13 | export function useAuth() { |
| 14 | const auth = inject(AuthService); |
| 15 | const router = inject(Router); |
| 16 | return { login: auth.login.bind(auth), goHome: () => router.navigate(['/']) }; |
| 17 | } |
Interactive Sandbox
โ Move something, see it react instantly.Pattern
Injector tree โ toggle which level provides MyService
When to use
For stateful services that should be shared globally: auth, HTTP clients, logging, analytics, feature flags. providedIn: 'root' is the default choice for most services.
| 1 | import { Injectable } from '@angular/core'; |
| 2 | ย |
| 3 | // providedIn: 'root' is the most common pattern |
| 4 | // One instance for the entire app โ shared state |
| 5 | @Injectable({ providedIn: 'root' }) |
| 6 | export class AuthService { |
| 7 | private currentUser: User | null = null; |
| 8 | ย |
| 9 | login(creds: Credentials): Observable<User> { /* ... */ } |
| 10 | logout(): void { this.currentUser = null; } |
| 11 | getUser(): User | null { return this.currentUser; } |
| 12 | } |
| 13 | ย |
| 14 | // Usage โ Angular injects the singleton automatically |
| 15 | @Component({ standalone: true, template: '' }) |
| 16 | export class HeaderComponent { |
| 17 | constructor(private auth: AuthService) {} |
| 18 | // OR modern functional injection (Angular 14+): |
| 19 | // private auth = inject(AuthService); |
| 20 | } |
| 21 | ย |
| 22 | // Also available: providedIn: 'platform' โ shared across apps in a |
| 23 | // multi-app shell. providedIn: 'any' โ unique instance per lazy module. |
Challenge
Explore all 5 DI patterns. Use the tree visualizer to understand how Angular resolves MyService at each level.
Why Should I Care?
โ The exact interview question + the bug it kills.Interview questions
Q: What's the difference between providedIn: 'root' and component-level providers?
providedIn: 'root' creates one singleton shared across the entire app โ it's tree-shakeable and lives for the app lifetime. Component providers: [] create a new instance for each component instance โ scoped to that component's subtree and destroyed when the component is destroyed. Use root for shared state (auth, HTTP), use component providers for instance-specific state (form wizard state, per-widget state).
Q: Why use InjectionToken instead of a class?
Classes can only be injected as-is. InjectionToken works for non-class values: config objects, strings, booleans, factory functions, arrays (multi: true). It's also the correct tool when you want multiple implementations of the same interface โ TypeScript interfaces are erased at runtime so they can't serve as DI tokens. InjectionToken provides a stable runtime identity independent of any class.
Q: What is the Angular injector hierarchy?
Angular has two parallel injector trees. The element injector tree mirrors the component tree โ each component has its own injector that checks its providers: [] first. The environment injector tree covers NgModules and ApplicationConfig-level providers. When a component requests a service, Angular walks the element injector tree up to root, then up the environment injector chain to the platform injector. If still not found: NullInjectorError.
The Deep Dive
โ Spec refs, engine internals, the minutiae.providedIn variants โ 'root' vs 'platform' vs 'any'
useFactory with deps โ dynamic service creation
| 1 | // Factory provider โ full control over instantiation |
| 2 | { |
| 3 | provide: DataService, |
| 4 | useFactory: (http: HttpClient, config: AppConfig) => { |
| 5 | return config.useMock |
| 6 | ? new MockDataService() |
| 7 | : new DataService(http, config.apiBaseUrl); |
| 8 | }, |
| 9 | deps: [HttpClient, APP_CONFIG], // injected as factory arguments |
| 10 | } |
| 11 | ย |
| 12 | // Multi providers โ aggregate multiple values under one token |
| 13 | export const HTTP_INTERCEPTORS = new InjectionToken<HttpInterceptor[]>( |
| 14 | 'HTTP_INTERCEPTORS' |
| 15 | ); |
| 16 | providers: [ |
| 17 | { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, |
| 18 | { provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true }, |
| 19 | // inject(HTTP_INTERCEPTORS) โ [AuthInterceptor, LoggingInterceptor] |
| 20 | ] |
inject() in standalone contexts
inject() works in injection context: inside class constructors, class field initializers, provider factories, and Angular hooks. It's the enabling primitive for composable utility functions โ extracting DI logic out of constructors into reusable functions, similar to React hooks. Key: inject() throws if called outside an injection context (e.g., inside setTimeout or async callbacks).
| 1 | // Composable DI utility โ works like a custom React hook |
| 2 | export function injectBreakpoint() { |
| 3 | const breakpointObserver = inject(BreakpointObserver); |
| 4 | return breakpointObserver.observe([Breakpoints.Handset]).pipe( |
| 5 | map((state) => state.matches), |
| 6 | ); |
| 7 | } |
| 8 | ย |
| 9 | // Used in any component or service |
| 10 | @Component({ template: '' }) |
| 11 | export class ResponsiveComponent { |
| 12 | isMobile$ = injectBreakpoint(); // inject() called during field init |
| 13 | } |
Interview Questions
โ Real questions from real interviews โ with answers.Root = app-wide singleton; component providers = new instance per component instance, destroyed with it.
InjectionToken works for non-class values (configs, arrays, primitives) and avoids TypeScript interface erasure.
Element injectors (child โ parent โ ... โ root) first, then environment injectors, then NullInjector.
inject() works in field initializers and standalone functions โ enabling composable utility hooks.
useFactory: custom instantiation logic or dynamic choice of implementation. useExisting: alias one token to another.
Lazy routes create their own environment injector โ services provided there are NOT shared with root.
Memory Game
โ Quick quiz โ lock the concept in long-term memory.What is the platform injector in Angular's DI hierarchy?