📌 20 de Outubro, 2024

Angular: Run Multiple APP_INITIALIZER sequentially

Informática · Programação

📌 20 de Outubro, 2024

Angular: Run Multiple APP_INITIALIZER sequentially

Informática · Programação

In Angular the APP_INITIALIZER is designed for running asynchronous code during the application’s initialization phase – it waits for an observable or promise to complete before running code from your main app component. While this feature is quite useful, adding more than one APP_INITIALIZER will cause them to run in parallel, which makes it difficult to manage dependencies between them.

The Problem

Let’s consider we’ve three different services ConfigService, LoggingService and AppAnalyticsService:

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class ConfigService implements SeqInitInterface {
  init(): Observable<any> {
    console.log('Initializing ConfigService...');
    return of(true); // Replace with your initialization logic
  }
}

@Injectable({
  providedIn: 'root',
})
export class LoggingService implements SeqInitInterface {
  init(): Observable<any> {
    console.log('Initializing LoggingService...');
    return of(true); // Replace with your initialization logic
  }
}

@Injectable({
  providedIn: 'root',
})
export class AppAnalyticsService implements SeqInitInterface {
  init(): Observable<any> {
    console.log('Initializing AppAnalyticsService...');
    return of(true); // Replace with your initialization logic
  }
}

Let’s say we need to initialize all of those before the app runs anything. One way to do it is to add multiple APP_INITIALIZER declarations to the providers.

However, if AppAnalyticsService depends on settings loaded by ConfigService, this approach will fail. Angular will initialize AppAnalyticsService at the same time as ConfigService, meaning that the settings won’t be available when AppAnalyticsService‘s initialization method runs.

Solution: Sequential Initialization Service

In order to avoid this pitfall we can implement a SequentialInitializerService, a service that will be responsible for loading up our services in a controlled and sequential way so our dependencies are predictable:

import { Injectable, Injector } from '@angular/core';
import { Observable, concatMap, of, from } from 'rxjs';

export interface SeqInitInterface {
   init(): Observable<any>;
}

@Injectable({
  providedIn: 'root',
})
export class SequentialInitializerService {
  constructor(private injector: Injector) {}

  // This will run each service sequentially and return a promise
  initialize(sequentialInitializers: any[]): Promise<any> {
    return new Promise((resolve, reject) => {
      from(sequentialInitializers)
        .pipe(
          concatMap(service => {
            const serviceInstance = this.injector.get(service);
            console.log(`Initializing ${service.constructor.name}...`);
            return serviceInstance.init(); // Each init returns an Observable
          })
        )
        .subscribe({
          complete: () => {
            console.log('All services initialized.');
            resolve(true);
          },
          error: (err) => {
            console.error('Error during initialization:', err);
            reject(err);
          }
        });
    });
  }
}

Now in our main.ts file we may load it like this:

export const appInitializers = [
  ConfigService,
  LoggingService,
  AppAnalyticsService 
];

bootstrapApplication(AppComponent, {
    providers: [
        importProvidersFrom(AppRoutingModule, BrowserModule, provideHttpClient(withInterceptorsFromDi()),
        {
            provide: APP_INITIALIZER,
            useFactory: (initService: SequentialInitializerService): () => initService.initialize(appInitializers);
            deps: [SequentialInitializerService],
            multi: true,
         }
    ]
}).catch(err => console.error(err));

That’s it! A simple and scalable solution for the problem.