How to run long tasks in Angular environment injector

Reading Time: 5 minutes

Loading

Introduction

Angular 14 introduced ENVIRONMENT_INITIALIZER token that enables developers to run long tasks during the construction of Angular environment injector. 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.

let's go

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 exception.

// 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() that includes 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.

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:

  1. Github Repo: https://github.com/railsstudent/ng-environment-initializer-demo
  2. Stackblitz: https://stackblitz.com/edit/stackblitz-starters-7ptzkm?file=src%2Fmain.ts
  3. Hierarchical dependency injection: https://www.blueskyconnie.com/mastering-angular-hierarchical-dependency-injection-with-inject-function/