How to run long tasks in Angular environment injector

railsstudent

Connie Leung

Posted on September 23, 2023

How to run long tasks in Angular environment injector

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:

💖 💪 🙅 🚩
railsstudent
Connie Leung

Posted on September 23, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related