Angular (v5) service is getting constructed before APP_INITIALIZER promise resolves
Asked Answered
M

7

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?

Melvinamelvyn answered 8/3, 2018 at 0:29 Comment(4)
Where do you use SomeOtherService. Can you put up reproduction?Cykana
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?Ruler
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 answerHorology
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?Weathersby
A
13

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

Alagoas answered 21/3, 2018 at 23:42 Comment(8)
Hey, can you please elaborate why his code is not working? Here is an example stackblitz.com/edit/angular-3zszwn?file=app%2Fapp.component.tsCykana
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. Check if you are doing something else in your code. Are you using the some service also in an initializer?Mendenhall
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.Melvinamelvyn
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?Mendenhall
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?Graduation
@Graduation 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?Mendenhall
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.Beckford
Can you make a StackBlitz example to reproduce the issue?Mendenhall
A
3

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');
      })
    );
  }
}
Auberbach answered 22/3, 2018 at 9:50 Comment(6)
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)Auberbach
Ok, then i need to ask AlesD:)Cykana
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?Auberbach
What is this then? return new Promise and then he calls resolve()Cykana
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().Wast
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:-1Melvinamelvyn
C
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.

Chippy answered 27/3, 2018 at 11:32 Comment(3)
Not working for me. Still instantiating services in my app before the response from mycoolapp.com/env arrivesMelvinamelvyn
Calling res.json() won't work with new HttpClient, and result won't be of type ResponseRuler
Yes, you are right. This example is from Angular4. I will update it. Thanks DavidChippy
G
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.

Geehan answered 22/3, 2018 at 8:4 Comment(0)
M
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.

Matthaus answered 28/3, 2018 at 8:46 Comment(1)
there is no need for the map. thanks, your solution work perfectMarsiella
G
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

Gens answered 23/1, 2020 at 13:2 Comment(1)
Solution provided in above issue worked for me when you have this issue with NGRX effects. github.com/brandonroberts/effects-issue-exampleAlsatian
O
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.

Orderly answered 28/10, 2021 at 8:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.