AcademyAngular
Dependency Injection and Injection Tokens
inject() asks Angular's container for a shared instance instead of constructing one privately. InjectionToken<T> does the same for values that aren't classes.
2 min readUpdated 3 exercises
new'ing a service privately defeats the whole point of DI
inject() (or a constructor parameter, the older syntax for the same mechanism) asks Angular's container for the instance it already manages, rather than creating a fresh one:
@Injectable({ providedIn: 'root' })
export class CartService {
private items = signal<Item[]>([]);
}
// in a component
private cart = inject(CartService);A cart that "forgets" items added from one component when viewed from another is, almost every time, new CartService() written directly somewhere instead of inject(CartService) — each new call produces its own private state, so two components never actually share the one cart the app assumes exists. Nothing about that fails to compile; it just quietly stops behaving like a singleton.
providedIn: 'root' is what makes a service a real singleton
@Injectable({ providedIn: 'root' }) registers the service with the application's root injector once, and every inject() call anywhere in the app resolves to that same instance. Providing the same service again at a component level — in that component's own providers array — creates a second, scoped instance for that component's subtree instead, which is a deliberate, different pattern (a fresh instance per usage) rather than a bug, but it is easy to reach for by accident and then wonder why state stopped being shared.
InjectionToken<T> is DI for values that have no class to inject
Not everything worth injecting is a class — a config object, a base URL, a feature flag — and InjectionToken<T> gives those values the same DI treatment:
export const API_CONFIG = new InjectionToken<{ baseUrl: string }>('API_CONFIG');
// registered somewhere, e.g. in app config
{ provide: API_CONFIG, useValue: { baseUrl: 'https://api.example.com' } }
// consumed
private config = inject(API_CONFIG);A token that inject() reaches for but that throws at runtime almost always means the token was declared and injected, but never actually given a value anywhere in the provider list — the token itself is just a unique identity object; without a matching provide entry, there is nothing for the injector to resolve it to.
What interviewers check
Whether providedIn: 'root' vs. a component-level provider is a distinction you reach for deliberately, and whether you can explain why an InjectionToken needs an explicit provide entry when a plain @Injectable class does not.
Now practice it
1 quiz · 2 drillsReading this page is the cheap half. These are the exercises that make you use it.
- Dependency Injection with inject()ProFix a cart service that every component new’s up privately instead of sharing the one instance through DI.DrillSolve · MediumDrillSolve · Medium
- A Typed DI Token with InjectionToken<T>ProFix a config InjectionToken that inject() reaches for, but that nothing has ever provided a value for.DrillSolve · HardDrillSolve · Hard
- Services & DI · Hierarchical InjectorsInjector-hierarchy questions at the level where providedIn choices start to matter.QuizTake · MediumQuizTake · Medium
Read next
Input, Output and Content Projection
@Input()/@Output() pass data down and events up between components; <ng-content> lets a parent's markup render inside a child it doesn't control.
2 min
Attribute Fallthrough and ngOnChanges
A class or attribute a caller puts on a component's host element falls through by default; @HostBinding sets one from inside. ngOnChanges reports what changed.
2 min