33

I have a Angular 5.2.0 application. I looked up how to implement APP_INITIALIZER to load configuration information before the app starts. Here an extract of the app.module:

providers: [
    ConfigurationService,
    {
        provide: APP_INITIALIZER,
        useFactory: (configService: ConfigurationService) =>
            () => configService.loadConfigurationData(),
        deps: [ConfigurationService],
        multi: true
    }
],

Here the configuration.service:

import { Injectable, Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { Configuration } from './configuration';

@Injectable()
export class ConfigurationService {
    private readonly configUrlPath: string = 'Home/Configuration';
    private configData: Configuration;

    constructor(
        private http: HttpClient,
        @Inject('BASE_URL') private originUrl: string) { }

    loadConfigurationData() {
        this.http
            .get<Configuration>(`${this.originUrl}${this.configUrlPath}`)
            .subscribe(result => {
                this.configData = {
                    test1ServiceUrl: result["test1ServiceUrl"],
                    test2ServiceUrl: result["test2ServiceUrl"]        
                }
            });
    }

    get config(): Configuration {
        return this.configData;
    }
}

Here is an example of a constructor of a component where the configData is used:

export class TestComponent {
    public test1ServiceUrl: string;

    constructor(public configService: ConfigurationService) {
        this.test1ServiceUrl = this.configService.config.test1ServiceUrl;
    }
}

It works fine with all the components which are defined within the <router-outlet></router-outlet>. But the same implementation in a component outside the <router-outlet></router-outlet> does not work.
When I debug the respective constructor of the component where it does not work it says that configService is null.
Why is the APP_INITIALIZER executed before the constructor of a component inside the <router-outlet></router-outlet> is called but not before the constructor of a component outside the <router-outlet></router-outlet>?

Stefan Falk
  • 23,898
  • 50
  • 191
  • 378
Palmi
  • 2,381
  • 5
  • 28
  • 65
  • 1
    I just discovered the APP_INITIALIZER on another thread and I'm mad that after 6 years of working with angular I haven't known about it. It's going to solve a lot of problems I have. But, the Angular docs provide absolutely zero information on it. I opened up a request with the angular team to get some info on it (https://github.com/angular/angular/issues/34703). – mwilson Jan 09 '20 at 17:42

3 Answers3

39

Due to how APP_INTIALIZER works, it's expected that asynchronous initializers return promises, but your implementation of APP_INTIALIZER multiprovider doesn't because loadConfigurationData function doesn't return anything.

It should be something like:

loadConfigurationData(): Promise<Configuration> {
  return this.http.get<Configuration>(`${this.originUrl}${this.configUrlPath}`)
  .do(result => {
    this.configData = result;
  })
  .toPromise();
}
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • How do I do that with HttpClient from @angular/common/http? – Palmi Apr 07 '18 at 18:59
  • 1
    It says: (TS) Property 'do' does not exist on type 'Observable – Palmi Apr 07 '18 at 19:08
  • 2
    This is common Angular/RxJS knowledge that isn't specific to this case. If you use RxJS operator, you need to import it first. You likely did this for other operators. There should be something like `import 'rxjs/add/operator/do`. See https://stackoverflow.com/questions/42376972/best-way-to-import-observable-from-rxjs – Estus Flask Apr 07 '18 at 19:18
  • 4
    One important 'safety tip'. You really MUST return a Promise... trying to return the observable directly does not work and it doesn't tell you that it won't work, it just... doesn't work. – Reginald Blue Aug 19 '19 at 16:09
  • 12
    `pipe`/`tap` should be used now, instead of `do`. – lealceldeiro Nov 10 '19 at 09:16
  • 1
    + lastValueFrom(...) instead of (...).toPromise() – noamyg Oct 19 '22 at 14:27
4

Do something like

 export function StartupServiceFactory(startupService: StartupService) {
      return () => startupService.load();
    }
    const APPINIT_PROVIDES = [
      StartupService,
      {
        provide: APP_INITIALIZER,
        useFactory: StartupServiceFactory,
        deps: [StartupService],
        multi: true,
      },
    ];

Startup service

load():Promise{
    return new Promise(resolve, reject){
        //load all your configuration 
         resolve(); 
    }

  }
Pir Abdul
  • 2,274
  • 1
  • 26
  • 35
-3

I had a similar problem. The fix was much the same, actually return a promise. see: ngOnInit starts before APP_INITIALIZER is done

Chip
  • 220
  • 5
  • 17