How to run long tasks in Angular environment injector
Connie Leung
Posted on September 23, 2023
Introduction
Angular 14 introduced ENVIRONMENT_INITIALIZER
token that enables developers to run long tasks during Angular application startup. For standalone component, I inject the new token in the providers array and pass the provider to bootstrapApplication()
function. Moreover, I found a use case of hierarchical dependency (explained here) where inject()
ensures this provider is called exactly once.
In this blog post, I describe how to use ENVIRONMENT_INITIALIZER
in Angular environment injector and apply inject(provider token, injection options)
to avoid repeated injections of the token.
Demo of ENVIRONMENT_INITIALIZER
In this demo, I want to inject ENVIRONMENT_INITIALIZER
and provide a function that loads user preferences from a remote data source. After retrieving the remote data, Angular component uses the preferences to update CSS styles. Moreover, inject()
function guards the provider by passing { skipSelf: true, optional: true } option
. If this provider is lazy loaded, inject()
returns a non-null value and throws an error.
// preferences.json
{
"preferences": {
"top": {
"backgroundColor": "yellow",
"border": "1px solid black",
"color": "rebeccapurple",
"fontSize": "36px",
"textAlign": "center"
},
"content": {
"backgroundColor": "cyan",
"border": "1px solid black"
},
"label": {
"color": "gray",
"size": "18px"
},
"font": {
"color": "rebeccapurple",
"fontSize": "18px",
"fontStyle": "italic",
"fontWeight": "600"
}
}
}
The above JSON is a dummy user preferences that I created in my Github gist. I am going to use HttpClient to retrieve the data when application is loading.
// main.ts
@Component({
selector: 'my-app',
standalone: true,
imports: [UserProfileComponent],
template: '<app-user-profile></app-user-profile>',
})
export class App {}
bootstrapApplication(App, {
providers: [
provideHttpClient(),
provideRouter(APP_ROUTES),
providerCore()
]
});
The core logic lies in providerCore()
the initialization of URL, construction of environment injector, and guards against lazy loaded injection.
Declare injection tokens
I am going to create a core folder, and put injection tokens, providers and service there.
First, I define two injection tokens, CORE_GUARD
and PREFERENCE_URL
.
// core-guard.token.ts
export const CORE_GUARD = new InjectionToken<string>('CORE_GUARD');
CORE_GUARD
is a guard token to prevent ENVIRONMENT_INITIALIZER
from injecting two or more times.
// preference-url.token.ts
export const PREFERENCE_URL = new InjectionToken('PREFERENCE_URL');
PREFERENCE_URL
injects the URL to retrieve the user preferences
Declare service to retrieve user preferences
Before passing providers to initialize the application, I add a service to retrieve the user preferences and store the results in a Subject. I would love to use Signal but I cannot think of a reasonable initial value.
import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { delay, map, Subject } from 'rxjs';
import { PREFERENCE_URL } from '../injection-tokens/preference-url.token';
import { PreferencesHolder, UserStyles } from '../interfaces/preferences.interface';
@Injectable({
providedIn: 'root'
})
export class SettingsService {
private readonly httpClient = inject(HttpClient);
private readonly stylesSub = new Subject<UserStyles>();
styles$ = this.stylesSub.asObservable();
private url = inject(PREFERENCE_URL);
private load$ = this.httpClient.get<PreferencesHolder>(this.url)
.pipe(
delay(800),
map(({ preferences }) => preferences),
takeUntilDestroyed(),
);
load() {
this.load$.subscribe((styles) => {
this.stylesSub.next(styles);
console.log('Application styles are loaded successfully', styles);
});
}
}
The load$
Observable returns the CSS styles of font, label, top row and content. Moreover, load()
updates stylesSub
subject that emits the object to styles$
Observable. Our component makes use of styles$
to update the CSS styles of the HTML elements.
Define Core Providers to construct Angular Environment Injector
providerCore
is a function that returns an array of Providers. The interesting ones are the providers that inject CORE_GUARD and ENVIRONMENT_INITIALIZER respectively.
// app.route.ts
export const APP_ROUTES: Route[] = [{
path: 'lazy',
loadComponent: () => import('./users/lazy-loaded/lazy-loaded.component')
.then(mod => mod.LazyLoadedComponent)
}];
export function providerCore(): (EnvironmentProviders | Provider)[] {
return [
{
provide: CORE_GUARD,
useValue: 'CORE_GUARD'
},
{
provide: PREFERENCE_URL,
useValue: 'https://gist.githubusercontent.com/railsstudent/7c8d4b6b6158812e02ca8efcc5259127/raw/3190954f22a439a9df00ed7377daa5a05a3c32b9/preferences.json',
},
{
provide: ENVIRONMENT_INITIALIZER,
multi: true,
useValue: () => {
const coreGuard = inject(CORE_GUARD, {
skipSelf: true,
optional: true,
});
console.log('coreGuard', coreGuard);
if (coreGuard) {
throw new TypeError('providerCore cannot load more than once.');
}
inject(SettingsService).load();
}
}
];
}
CORE_GUARD
is injected and then used in the provider of ENVIRONMENT_INITIALIZER
const coreGuard = inject(CORE_GUARD, {
skipSelf: true,
optional: true,
});
When bootstrapApplication
invokes providerCore()
, coreGuard
is null and error does not occur. When LazyLoadedComponent
provides providerCore()
, coreGuard
equals to CORE_GUARD and throws TypeError.
inject(SettingsService).load();
calls SettingsService to store CSS styles in the subject
Apply application data to components
// user-profile.component.ts
@Component({
selector: 'app-user-profile',
standalone: true,
imports: [NgStyle, RouterLinkActive, RouterLink, RouterOutlet],
template: `...inline template...`,
})
export class UserProfileComponent {
styles$ = inject(SettingsService).styles$;
stylesSignal = toSignal(this.styles$);
topSignal = computed(() => this.stylesSignal()?.top);
contentSignal = computed(() => this.stylesSignal()?.content);
labelSignal = computed(() => this.stylesSignal()?.label);
fontSignal = computed(() => this.stylesSignal()?.font);
}
I use toSignal
to convert styles$ Observable to stylesSignal
, and compute new signals from it. Then, these signals can bind to styles to change the appearances of the Div, Span and Label elements respectively.
template: `
<div>
<div class="banner" [style]="topSignal()">
My banner
</div>
<div class="info" [style]="contentSignal()">
<div class="row">
<label for="name" [style]="labelSignal()">Name: </label>
<span id="name" name="name" [style]="fontSignal()">Mary Doe</span>
</div>
<div class="row">
<label for="gender" [style]="labelSignal()">Gender : </label>
<span id="gender" name="gender" [style]="fontSignal()">Female</span>
</div>
<div class="row">
<label for="languages" [style]="labelSignal()">Languages : </label>
<span id="name" name="name" [style]="fontSignal()">Cantonese, English, Mandarin, Spanish</span>
</div>
</div>
</div>
`,
For example, [style]=topSignal()
alters the appearance of the Div element to add black border and yellow background, centre text and enlarge the font size to 36px.
{
"backgroundColor": "yellow",
"border": "1px solid black",
"color": "rebeccapurple",
"fontSize": "36px",
"textAlign": "center"
}
What happens when I create lazy injector to provide providerCore?
Guard against ENVIRONMENT_INITIALIZER in lazy loaded injector
In UserProfileComponent
, I put a RouteLink
to lazy load LazyLoadedComponent
. This component is a bare bone component except I provide providerCore()
in Injector.create
.
// user-profile.component.ts
<ul>
<li>
<a routerLink="/lazy" routerLinkActive="active">Lazy load standalone component and providerCore throws error</a>
</li>
</ul>
<router-outlet></router-outlet>
// lazy-loaded.component.ts
import { Component, inject, Injector } from '@angular/core';
import { providerCore } from '../../core';
@Component({
selector: 'app-lazy-loaded',
standalone: true,
template: '<p>lazy-loaded works!</p>',
})
export class LazyLoadedComponent {
parentInjector = inject(Injector);
lazyLoadedInjector = Injector.create({
providers: [providerCore()],
parent: this.parentInjector
});
}
lazyLoadedInjector
inherits from its parent injector, parentInjector
, and throws error because the value of CORE_GUARD injection token is CORE_GUARD.
In the console of Chrome DevTool, TypeError is logged.
ERROR Error: Uncaught (in promise): TypeError: providerCore cannot load more than once.
TypeError: providerCore cannot load more than once.
at useValue (core.provider.ts:30:17)
at R3Injector.resolveInjectorInitializers (core.mjs:9335:17)
at createInjector (core.mjs:10438:14)
at _Injector.create (core.mjs:10488:20)
at new _LazyLoadedComponent (lazy-loaded.component.ts:14:23)
at NodeInjectorFactory.LazyLoadedComponent_Factory [as factory] (lazy-loaded.component.ts:19:4)
at getNodeInjectable (core.mjs:4738:44)
at createRootComponent (core.mjs:14236:35)
at ComponentFactory.create (core.mjs:14100:25)
at ViewContainerRef2.createComponent (core.mjs:24413:47)
at resolvePromise (zone.js:1193:31)
at resolvePromise (zone.js:1147:17)
at zone.js:1260:17
at _ZoneDelegate.invokeTask (zone.js:402:31)
at core.mjs:10715:55
at AsyncStackTaggingZoneSpec.onInvokeTask (core.mjs:10715:36)
at _ZoneDelegate.invokeTask (zone.js:401:60)
at Object.onInvokeTask (core.mjs:11028:33)
at _ZoneDelegate.invokeTask (zone.js:401:60)
at _Zone.runTask (zone.js:173:47)
The following Stackblitz repo shows the final results:
This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular and other technologies.
Resources:
- Github Repo: https://github.com/railsstudent/ng-environment-initializer-demo
- Stackblitz: https://stackblitz.com/edit/stackblitz-starters-7ptzkm?file=src%2Fmain.ts
- Hierarchical dependency injection: https://dev.to/railsstudent/mastering-angulars-hierarchical-dependency-injection-with-inject-function-1e5g
Posted on September 23, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.