🚀 Angular DI & Injection Context

Dependency Injection in Standalone Applications

Checked against Angular 22 Standalone Zoneless Signals

1. Injection Context - Complete Overview

graph TD A[Injection Context] --> B{Context Types} B --> C[Constructor Context] B --> D[Field Initializer Context] B --> E[Factory Function Context] B --> F[Guard/Resolver Context] B --> T[runInInjectionContext] C --> G[Component Constructor] C --> H[Service Constructor] C --> I[Directive Constructor] D --> J[Class Fields] D --> K[Property Declaration] E --> L[useFactory Provider] E --> M[InjectionToken Factory] F --> N[Route Guards] F --> O[Route Resolvers] G --> P[inject Function Available] H --> P I --> P J --> P K --> P L --> P M --> P N --> P O --> P T --> P P --> Q{Valid Context?} Q -->|Yes| R[Service Instance Returned] Q -->|No| S[Error: inject requires context]
💡 Key Point: The inject() function can only be called in an injection context. This includes constructors, field initializers, factory functions, and functional guards and resolvers. Lifecycle hooks such as ngOnInit are not injection contexts: call inject() in a field or the constructor, or use runInInjectionContext.

2. Service Creation & Registration Flow

graph TD A[Define Service Class] --> B{Injectable Decorator} B --> C[providedIn Option] C --> D{Scope Type?} D -->|root| E[Root Injector
Application-wide Singleton] D -->|platform| F[Platform Injector
Shared across apps] D -->|any| G[Deprecated
Do Not Use] D -->|null| H[Must Provide Manually] E --> I[Tree-shakeable] F --> I H --> J[Not Tree-shakeable] G --> I I --> K[Service Ready] J --> K K --> L{How to Provide?} L -->|bootstrapApplication| M[Global Providers Array] L -->|Component| N[Component Providers] L -->|Route| O[Route Providers] M --> P[Application Scope] N --> Q[Component Tree Scope] O --> R[Route Tree Scope] P --> S[Service Instance Created on First Use] Q --> S R --> S

Service Definition Examples:

// 1. Root-level singleton service (recommended) @Injectable({ providedIn: 'root' }) export class UserService { private users = signal<User[]>([]); constructor() { console.log('UserService created - singleton instance'); } } // 2. Component-level service @Injectable() export class FormStateService { private formData = signal({}); } @Component({ selector: 'app-user-form', providers: [FormStateService], // New instance per component template: '...' }) export class UserFormComponent {} // 3. Route-level service export const userRoutes: Routes = [ { path: 'users', providers: [UserRouteService], // Shared in route tree loadComponent: () => import('./user.component') } ];

3. Service Resolution & Injector Hierarchy

graph TB A[Component Requests Service
inject MyService] --> B{Check Component Injector} B -->|Found| C[Return Instance] B -->|Not Found| D{Check Parent Component} D -->|Found| E[Return Instance] D -->|Not Found| F{Check Route Injector} F -->|Found| G[Return Instance] F -->|Not Found| H{Check Root Injector} H -->|Found| I[Return Instance] H -->|Not Found| J{Check Platform Injector} J -->|Found| K[Return Instance] J -->|Not Found| L[NullInjectorError] C --> M[Use Service] E --> M G --> M I --> M K --> M M --> N{Service Exists?} N -->|No| O[Create New Instance] N -->|Yes| P[Return Cached Instance] O --> Q[Call Constructor] Q --> R[Resolve Dependencies] R --> S[Cache Instance] S --> T[Return to Component] P --> T
⚠️ Important: The injector hierarchy determines service scope and lifetime. Services are cached at the injector level where they're provided.

4. Component & Service Integration Flow

graph TD A[Component Creation] --> B[Constructor Phase] B --> C[Injection Context Active] C --> D[inject Function Calls] D --> E[Service 1: inject UserService] D --> F[Service 2: inject HttpClient] D --> G[Service 3: inject Custom] E --> H[Resolve from Injector] F --> H G --> H H --> I{Instance Exists?} I -->|Yes| J[Return Cached] I -->|No| K[Create New] K --> L[Service Constructor] L --> M[Service Dependencies] M --> N[Service Ready] J --> O[All Services Injected] N --> O O --> P[Component Fields Init] P --> Q[Signals Setup] Q --> R[Effects Setup] R --> S[ngOnInit] S --> T[Component Ready] T --> U[Service Methods Available] U --> V[Component Lifecycle]

Complete Integration Example:

@Component({ selector: 'app-user-list', template: ` <div class="user-list"> @if (loading()) { <div>Loading users...</div> } @for (user of users(); track user.id) { <div class="user-card"> {{ user.name }} - {{ user.role }} </div> } <button (click)="refreshUsers()">Refresh</button> </div> ` }) export class UserListComponent { // 1. Services injection (in injection context) private userService = inject(UserService); private http = inject(HttpClient); private router = inject(Router); // 2. State signals users = signal<User[]>([]); loading = signal(false); // 3. Computed values from service totalUsers = computed(() => this.users().length); // 4. Effects for reactive updates constructor() { // Still in injection context const destroyRef = inject(DestroyRef); // Auto-sync with service state effect(() => { const serviceUsers = this.userService.getUsers(); this.users.set(serviceUsers); }); // Cleanup on destroy destroyRef.onDestroy(() => { console.log('Component cleanup'); }); } // 5. Using service methods async refreshUsers() { this.loading.set(true); try { await this.userService.loadUsers(); } finally { this.loading.set(false); } } }

5. Advanced Injection Patterns

graph TD A[Advanced DI Patterns] --> B[Factory Providers] A --> C[InjectionToken] A --> D[Multi Providers] A --> E[Optional/Self/SkipSelf] B --> F[useFactory Function] F --> G[Dynamic Service Creation] C --> H[Type-safe Config] H --> I[APP_CONFIG Token] D --> J[Multiple Instances] J --> K[HTTP_INTERCEPTORS] E --> L[Decorator Modifiers] E --> L2[inject Options] L2 --> L3[optional, self, skipSelf, host] L --> M[@Optional] L --> N[@Self] L --> O[@SkipSelf] L --> P[@Host] G --> Q[Conditional Logic] I --> Q K --> Q Q --> R[Resolved Service]

Advanced Pattern Examples:

// 1. InjectionToken with Factory export const API_CONFIG = new InjectionToken<ApiConfig>('api.config', { providedIn: 'root', factory: () => ({ baseUrl: environment.apiUrl, timeout: 5000 }) }); // 2. Factory Provider with Dependencies export const loggerProvider = { provide: LoggerService, useFactory: (http: HttpClient, config: ApiConfig) => { return new LoggerService(http, config.logLevel); }, deps: [HttpClient, API_CONFIG] }; // 3. Multi Providers (class interceptors; new code usually prefers withInterceptors) export const authInterceptorProvider = { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }; // 4. Using in Component with Modifiers @Component({ selector: 'app-feature' }) export class FeatureComponent { // Optional injection - won't error if not found private optionalService = inject(OptionalService, { optional: true }); // Self injection - only from this component's injector private selfService = inject(LocalService, { self: true }); // Skip self - skip this component's injector private parentService = inject(SharedService, { skipSelf: true }); // Host injection - from host component private hostService = inject(HostService, { host: true }); constructor() { if (this.optionalService) { console.log('Optional service available'); } } }

6. Service Lifecycle & Memory Management

graph TD A[Service Request] --> B{First Request?} B -->|Yes| C[Create Instance] B -->|No| D[Return Cached] C --> E[Constructor Called] E --> F[Dependencies Injected] F --> G[Service Initialized] G --> H[Cache in Injector] H --> I[Return to Consumer] D --> I I --> J[Service in Use] J --> K{Scope Type?} K -->|Root| L[Lives Forever
Until App Destroy] K -->|Component| M[Lives with Component] K -->|Route| N[Route Environment Injector] M --> O{Component Destroyed?} O -->|Yes| P[Service Destroyed] O -->|No| J N --> Q{withAutoCleanupInjectors Enabled?} Q -->|Yes, Route Unused| R[Service Destroyed] Q -->|No| S L --> S[App Shutdown] S --> T[All Services Destroyed] P --> U[Garbage Collection] R --> U T --> U
✅ Best Practice: Use root-level services for shared state, component-level for local state, and always clean up subscriptions in services. Leaving a route does not destroy its providers unless you enable withAutoCleanupInjectors.

7. Testing Services & DI

// Testing Service with Dependencies describe('UserService', () => { let service: UserService; let httpMock: HttpTestingController; beforeEach(() => { TestBed.configureTestingModule({ providers: [ UserService, provideHttpClient(), provideHttpClientTesting() ] }); // Inject services in test context service = TestBed.inject(UserService); httpMock = TestBed.inject(HttpTestingController); }); it('should load users', () => { const mockUsers = [{ id: 1, name: 'John' }]; service.loadUsers(); const req = httpMock.expectOne('/api/users'); expect(req.request.method).toBe('GET'); req.flush(mockUsers); expect(service.users()).toEqual(mockUsers); }); }); // Testing Component with Mock Services describe('UserComponent', () => { let component: UserComponent; let mockUserService: { loadUsers: Mock }; beforeEach(() => { mockUserService = { loadUsers: vi.fn() }; // Vitest, the default runner for new projects TestBed.configureTestingModule({ providers: [ { provide: UserService, useValue: mockUserService } ] }); // Create component with mocked service component = TestBed.createComponent(UserComponent).componentInstance; }); it('should use injected service', () => { component.refreshUsers(); expect(mockUserService.loadUsers).toHaveBeenCalled(); }); });