33

I'm expecting Angular to wait until my loadConfig() function resolves before constructing other services, but it is not.

app.module.ts

export function initializeConfig(config: AppConfig){
    return () => config.loadConfig();
}

@NgModule({
     declarations: [...]
     providers: [
          AppConfig,
         { provide: APP_INITIALIZER, useFactory: initializeConfig, deps: [AppConfig], multi: true }
     ] })
export class AppModule {

}

app.config.ts

@Injectable()
export class AppConfig {

    config: any;

    constructor(
        private injector: Injector
    ){
    }

    public loadConfig() {
        const http = this.injector.get(HttpClient);

        return new Promise((resolve, reject) => {
            http.get('http://mycoolapp.com/env')
                .map((res) => res )
                .catch((err) => {
                    console.log("ERROR getting config data", err );
                    resolve(true);
                    return Observable.throw(err || 'Server error while getting environment');
                })
                .subscribe( (configData) => {
                    console.log("configData: ", configData);
                    this.config = configData;
                    resolve(true);
                });
        });
    }
}

some-other-service.ts

@Injectable()
export class SomeOtherService {

    constructor(
        private appConfig: AppConfig
    ) {
         console.log("This is getting called before appConfig's loadConfig method is resolved!");
    }
 }

The constructor of SomeOtherService is getting called before the data is received from the server. This is a problem because then the fields in SomeOtherService do not get set to their proper values.

How do I ensure SomeOtherService's constructor gets called only AFTER the loadConfig's request is resolved?

tobias47n9e
  • 2,233
  • 3
  • 28
  • 54
CodyBugstein
  • 21,984
  • 61
  • 207
  • 363
  • Where do you use SomeOtherService. Can you put up reproduction? – yurzui Mar 22 '18 at 10:07
  • 1
    Can you post a sample on stackblitz? @yurzi posted one with what seems to be the exact same code that you have and it works properly.Also, do you have any HttpInterceptors in your code? – David Mar 23 '18 at 09:04
  • Don't use the `.catch` there, since the APP_INITIALIZER has it's own catch that stops the APP. Also, use the `.toPromise()` from @AlesD answer – aemonge Oct 09 '18 at 15:42
  • I'm having a similar issue:( I have tried to reproduce it in a stackblitz but haven't succeed. It's frustrating. Did you finally solved yours? Did you find any possible cause for this and how to solve it? – lealceldeiro Nov 10 '19 at 08:22

7 Answers7

12

I had also a simmilar issue what solved the issue for me was to use Observable methods and operators to do everything. Then in the end just use the toPromise method of the Observable to return a Promise. This is also simpler because you don't need to create a promise yourself.

The AppConfig service will then look something like that:

import { Injectable, Injector } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { tap } from 'rxjs/operators/tap';

@Injectable()
export class AppConfig {

    config: any = null;

    constructor(
        private injector: Injector
    ){
    }

    public loadConfig() {
        const http = this.injector.get(HttpClient);

        return http.get('https://jsonplaceholder.typicode.com/posts/1').pipe(
          tap((returnedConfig) => this.config = returnedConfig)
        ).toPromise();
        //return from([1]).toPromise();
    }
}

I'm using the new pipeable operators in rxjs which is recommended by Google for Angular 5. The tap operator is equivalent to the old do operator.

I have also created a working sample on stackblitz.com so you can se it working. Sample link

Aleš Doganoc
  • 11,568
  • 24
  • 40
  • 1
    Hey, can you please elaborate why his code is not working? Here is an example https://stackblitz.com/edit/angular-3zszwn?file=app%2Fapp.component.ts – yurzui Mar 22 '18 at 10:34
  • 1
    If you write it the way i suggested does it work in your app? On stackblitz your code runs ok. If you look at the log the configData object is logged before the message in the service. I have created a fork where I added delay to the observable and you can clearly see that the application waits for the 10 seconds before starting and the config is available in the SomeOtherService. [link](https://stackblitz.com/edit/angular-wrndeb?file=app%2Fapp.config.ts). Check if you are doing something else in your code. Are you using the some service also in an initializer? – Aleš Doganoc Mar 22 '18 at 23:37
  • 8
    Hey @AlesD sorry for the delay. I tested this solution and it did not work. It entered the `loadConfig` but still loaded other services in my app before the HTTP request was returned. – CodyBugstein Mar 28 '18 at 06:05
  • Then you must be doing something else. Is there a possibility that you reproduce the issue on stackblitz.com by posting more of your code? – Aleš Doganoc Mar 31 '18 at 20:27
  • AlesD, i think you've misunderstood the question. Where in your code do you provide the 'some-other-service.ts' that reads the config in it's constructor? – jonas Oct 19 '18 at 08:34
  • @jonas I don't think so. I have now updated the StackBlitz sample in my answer to include some other service and call the config with a delay so the initializer takes longer and you can see that the constructor of the service is invoked after the initializer finishes and the config is there. I think he must be doing something else in his application because also his sample on StackBlitz works correctly. That is why I requested more information. Can you reproduce the issue? – Aleš Doganoc Oct 23 '18 at 20:59
  • 1
    I tried this but it doesn't work for me. I'm also using ngrx and have StoreModule.forRoot({}) and EffectsModule.forRoot([]) calls in app,module.ts which ideally shouldn't make any difference. – Rahul Misra Jan 29 '20 at 15:37
  • Can you make a StackBlitz example to reproduce the issue? – Aleš Doganoc Jan 30 '20 at 00:15
3
  async loadConfig() {
        const http = this.injector.get(HttpClient);

        const configData = await http.get('http://mycoolapp.com/env')
                    .map((res: Response) => {
                        return res.json();
                    }).catch((err: any) => {
                        return Observable.throw(err);
                    }).toPromise();
                this.config = configData;
        });
    }

The await operator is used to wait for a Promise. It can only be used inside an async function.

It is working fine.

Hardik Patel
  • 3,868
  • 1
  • 23
  • 37
2

Injector does not wait for observables or promises and there is no code that could make it happen.

You should use custom Guard or Resolver to ensure that config is loaded before initial navigation completes.

kemsky
  • 14,727
  • 3
  • 32
  • 51
2

First of all, you were really close to the right solution!

But before I explain, let me tell you that using subscribe into a service is often a code smell.

That said, if you take a look to the APP_INITALIZER source code it's just running a Promise.all on all the available initializer. Promise.all is itself waiting for all the promises to finish before continuing and thus, you should return a promise from your function if you want Angular to wait for that before bootstrapping the app.

So @AlesD's answer is definitely the right way to go.
(and I'm just trying to explain a bit more why)

I've done such a refactor (to use APP_INITALIZER) very recently into one of my projects, you can take a look to the PR here if you want.

Now, if I had to rewrite your code I'd do it like that:

app.module.ts

export function initializeConfig(config: AppConfig) {
  return () => config.loadConfig().toPromise();
}

@NgModule({
  declarations: [
    //  ...
  ],
  providers: [
    HttpClientModule,
    AppConfig,
    {
      provide: APP_INITIALIZER,
      useFactory: initializeConfig,
      deps: [AppConfig, HttpClientModule],
      multi: true,
    },
  ],
})
export class AppModule {}

app.config.ts;

@Injectable()
export class AppConfig {
  config: any;

  constructor(private http: HttpClient) {}

  // note: instead of any you should put your config type
  public loadConfig(): Observable<any> {
    return this.http.get('http://mycoolapp.com/env').pipe(
      map(res => res),
      tap(configData => (this.config = configData)),
      catchError(err => {
        console.log('ERROR getting config data', err);
        return _throw(err || 'Server error while getting environment');
      })
    );
  }
}
maxime1992
  • 22,502
  • 10
  • 80
  • 121
  • I think that AlesD has a stackblitz and for the why I've gave details about that in my answer (not returning a promise, but subscribing --> not ok) – maxime1992 Mar 22 '18 at 10:27
  • Ok, then i need to ask AlesD:) – yurzui Mar 22 '18 at 10:33
  • But... I just told you. He does not return a promise, that's it. What do you expect more than that + a working code example? – maxime1992 Mar 22 '18 at 11:05
  • What is this then? `return new Promise` and then he calls `resolve()` – yurzui Mar 22 '18 at 11:06
  • 2
    This is the correct answer. If the initializeConfig function returns a promise, the APP_INITIALIZER will wait until the promise resolves. And the right way to turn an observable into a promise is with .toPromise(). – GMK Mar 26 '18 at 19:04
  • This gives me errors `Cannot instantiate cyclic dependency! AppConfig ("[ERROR ->]"): in NgModule AppModule in ./AppModule@-1:-1 Cannot instantiate cyclic dependency! ApplicationRef ("[ERROR ->]"): in NgModule AppModule in ./AppModule@-1:-1` – CodyBugstein Mar 28 '18 at 06:09
2

I think you should not subscribe to the http get call but turn it into a promise before resolving the loadConfig promise, because the callback to subscribe may be called before the request returned and therefore resolves the promise to early. Try:

@Injectable()
export class AppConfig {

    config: any;

    constructor(
        private injector: Injector
    ){
    }

    public loadConfig() {
        const http = this.injector.get(HttpClient);

        return new Promise((resolve, reject) => {
            http.get('http://mycoolapp.com/env')
                .map((res) => res )
                .toPromise()
                .catch((err) => {
                    console.log("ERROR getting config data", err );
                    resolve(true);
                    return Observable.throw(err || 'Server error while getting environment');
                })
                .then( (configData) => {
                    console.log("configData: ", configData);
                    this.config = configData;
                    resolve(true);
                });
        });
    }
}

I only tried it with a timeout, but that worked. And I hope that toPromise() is at the correct position, due I'm not really using the map function.

tobias47n9e
  • 2,233
  • 3
  • 28
  • 54
Fussel
  • 1,740
  • 1
  • 8
  • 27
1

I'm facing a similar issue. I think the difference which wasn't announced here and causes that in other answers example works fine but not for the author is where SomeOtherService is injected. If it is injected into some other service it is possible that the initializer will not be resolved yet. I think the initializers will delay injecting services into components, not into other services and that will explain why it works in other answers. In my case, I had this issue due to https://github.com/ngrx/platform/issues/931

Maciej Sikorski
  • 743
  • 5
  • 16
  • Solution provided in above issue worked for me when you have this issue with NGRX effects. https://github.com/brandonroberts/effects-issue-example – Rajantha Fernando Mar 11 '21 at 06:14
1

I think you can check where "SomeOtherService" was called in call stack. In my case, besides APP_INITIALIZER, I also added HTTP_INTERCEPTORS where "SomeOtherService" is injected there. And that makes the service to be called before APP_INITIALIZER completes.

CAIsoul
  • 485
  • 5
  • 6