In this article, I want to talk about a feature which Angular provides, that isn’t particularly well known or used by many developers. When using Angular InjectionToken
, we can specify a factory function which returns a default value of the parameterized type T
. For example:
1 2 3 | const WINDOW = new InjectionToken<Window>('A reference to the window object', { factory: () => window, }); |
This sets up the InjectionToken
using this factory as a provider, as if it was defined explicitly in the application’s root injector. Now we can use it anywhere in our application:
1 2 3 4 5 6 | @Component({ selector: 'my-app' }) export class AppComponent { constructor(@Inject(WINDOW) window: Window) {} } |
But that’s not all. We can use the inject
function to obtain a reference to other providers inside our factory
function. Let’s see another real-world example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import { inject, InjectionToken } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; export type TimespanProvider = Observable<string>; export const TIMESPAN = new InjectionToken('Subscribe to timespan query param', { factory() { const activatedRoute = inject(ActivatedRoute); return activatedRoute.queryParams.pipe( pluck('timespan'), filterNil(), distinctUntilChanged() ); }, }); |
In the above example, we inject the ActivatedRoute
provider and return an observable for the timespan
query param. Now we can use it in our components:
1 2 3 4 5 6 7 8 9 10 | @Component({ selector: 'app-home' }) export class HomeComponent implements OnInit { constructor(@Inject(TIMESPAN) private timespan$: TimespanProvider) {} ngOnInit() { this.timespan$.pipe(untilDestroyed(this)).subscribe(console.log); } } |
We can also pass InjectionFlags
to the inject function. For example, we can say that the requested provider is optional
:
1 2 3 4 5 6 7 8 9 | import { inject, InjectionToken, InjectFlags } from '@angular/core'; const MY_PROVIDER = new InjectionToken('', { factory: () => { const optional = inject(SomeProvider, InjectFlags.Optional); return optional ?? fallback; }, }); |
Here’s another real-world example — let’s say you have a theme service, which exposes the current user’s theme:
1 2 3 4 5 6 7 8 9 | @Injectable({ providedIn: 'root' }) export class ThemeService { private theme = new Subject<string>(); theme$ = this.theme.asObservable(); setTheme(theme: string) { this.theme.next(theme); } } |
The only data that most components need is the current theme. So instead of doing the following in each component:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @Component({ selector: 'app-hello', template: `<h1>{{ theme$ | async }}</h1>` }) export class HelloComponent { theme$: Observable<string>; constructor(private themeService: ThemeService) {} ngOnInit() { this.theme$ = this.themeService.theme$; } } |
We can create a provider with the sole purpose of providing the current user’s theme:
1 2 3 4 5 6 | export type ActiveThemeProvider = Observable<string>; export const ACTIVE_THEME = new InjectionToken<ActiveThemeProvider>('Active theme', { factory() { return inject(ThemeService).theme$; } }); |
1 2 3 4 5 6 | @Component({ template: `<h1>{{ theme$ | async }}</h1>` }) export class HelloComponent { constructor(@Inject(ACTIVE_THEME) public theme$: ActiveThemeProvider) {} } |
In summary, the benefits of using the InjectionToken
factory function are:
- The provider is tree-shakeable, since we don’t need to inject it in our app module as we’d do with the
useFactory
provider. - Using
inject()
to request a provider is faster and more type-safe than providing an additional array of dependencies (which is the common usage ofuseFactory
providers). - The provider has a single responsibility, and our components are injected only with the data they need.
- It makes testing more straightforward because we don’t need to mock everything. We can return a mock value from the factory, and that’s all.