How to run long tasks in Angular environment injector

Connie Leung - Sep 23 '23 - - Dev Community

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"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
  ]
});
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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);
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
}];
Enter fullscreen mode Exit fullscreen mode
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();
      }
    }
  ];
}
Enter fullscreen mode Exit fullscreen mode

CORE_GUARD is injected and then used in the provider of ENVIRONMENT_INITIALIZER

const coreGuard = inject(CORE_GUARD, {
          skipSelf: true,
          optional: true,
});
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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>  
  `,
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
// 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
  });
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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:

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .