How to run long tasks in Angular environment injector
Tags: angular, typescript, webdev, tutorial
Published: 2023-09-23 06:00:18

##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](https://dev.to/railsstudent/mastering-angulars-hierarchical-dependency-injection-with-inject-function-1e5g)) 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. 

```json
// 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. 

```typescript
// 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`.

```typescript
// 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.

```typescript
// 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. 

```typescript
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.

```typescript
// app.route.ts

export const APP_ROUTES: Route[] = [{
  path: 'lazy',
  loadComponent: () => import('./users/lazy-loaded/lazy-loaded.component')
    .then(mod => mod.LazyLoadedComponent)
}];
```

```typescript
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`

```typescript
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.

```typescript
inject(SettingsService).load();
```
calls SettingsService to store CSS styles in the subject

###Apply application data to components

```typescript
// 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.  

```typescript
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. 

```json
{
	"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`.

```typescript
// 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>
```

```typescript
// 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.

```typescript
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:
{%embed https://stackblitz.com/edit/stackblitz-starters-7ptzkm?file=src%2Fmain.ts %}

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