codejump

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

Waiting at the end of this page.

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:

typescript
@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:

typescript
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 drills

Reading this page is the cheap half. These are the exercises that make you use it.