
Angular DI Essentials: What Changed the Game for Me
Most Angular devs use Dependency Injection every day without fully understanding it. Here are the key concepts that levelled up my Angular architecture.
Angular DI Essentials: What Changed the Game for Me
Most Angular devs use Dependency Injection every day without fully understanding it. Here's what changed the game for me 👇
✅ Services Are Your Modular Units
Services decorated with @Injectable are your modular units — keep business logic OUT of components. Common service patterns include data clients, state management, authentication, and logging.
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class UserService {
private users: User[] = [];
getUsers(): User[] {
return this.users;
}
addUser(user: User): void {
this.users = [...this.users, user];
}
}Your components should be thin — just wiring up templates to services:
import { Component, inject } from '@angular/core';
import { UserService } from './user.service';
@Component({
selector: 'app-user-list',
template: `
@for (user of users; track user.id) {
<div class="user-card">{{ user.name }}</div>
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserListComponent {
private userService = inject(UserService);
users = this.userService.getUsers();
}✅ The Injector Hierarchy Controls Instance Scope
The injector hierarchy (App → Route → Component) controls instance scope — one wrong level = shared state bugs.
// Root-level: ONE instance for the entire app
@Injectable({ providedIn: 'root' })
export class GlobalStateService {
count = signal(0);
}
// Component-level: NEW instance per component
@Component({
selector: 'app-counter',
providers: [CounterService], // Each component gets its own instance
template: `<span>{{ counter.value() }}</span>`,
})
export class CounterComponent {
counter = inject(CounterService);
}✅ inject() Over Constructor Injection
inject() over constructor injection = cleaner, more composable code. The inject() function works in field initializers, constructors, and even route guards:
// ❌ Old constructor injection
export class OldComponent {
constructor(
private userService: UserService,
private router: Router,
private http: HttpClient,
) {}
}
// ✅ Modern inject() approach
export class ModernComponent {
private userService = inject(UserService);
private router = inject(Router);
private http = inject(HttpClient);
}It also unlocks powerful patterns in functional guards and resolvers:
export const authGuard = () => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isAuthenticated()) {
return true;
}
return router.navigate(['/login']);
};✅ InjectionTokens Unlock Tree-Shaking
InjectionTokens unlock tree-shaking — unused code gets dropped from your bundle automatically.
import { InjectionToken } from '@angular/core';
export interface AppConfig {
apiUrl: string;
enableAnalytics: boolean;
maxRetries: number;
}
export const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG', {
providedIn: 'root',
factory: () => ({
apiUrl: 'https://api.myapp.com',
enableAnalytics: true,
maxRetries: 3,
}),
});
// Usage in a service
@Injectable({ providedIn: 'root' })
export class ApiClient {
private config = inject(APP_CONFIG);
fetch(endpoint: string) {
return `${this.config.apiUrl}/${endpoint}`;
}
}✅ skipSelf and host for Precision
skipSelf and host give you precision when the default resolution isn't enough.
@Component({
selector: 'app-panel',
providers: [PanelService],
template: `<app-panel-header />`,
})
export class PanelComponent {
panel = inject(PanelService); // Gets its own instance
}
@Component({
selector: 'app-panel-header',
template: `<h2>{{ panel.title() }}</h2>`,
})
export class PanelHeaderComponent {
// Skip own injector, use parent's PanelService instance
panel = inject(PanelService, { skipSelf: true });
}DI in Angular is genuinely one of the most well-engineered parts of the framework. The more you understand it, the more intentional your architecture becomes.
Save this if you're levelling up your Angular skills. 🚀


