15

Background: I need to perform couple of initial checks during the application start up (1) read angular app config from ./assets/config.json file and get the API end point from there, (2) make an API call to end point retrieved in first step and load some settings from back end.

Goal: be able to initialize two services using APP_INITIALIZER (say A & B), where B has a dependency on A. check out this stackblitz to see the problem

Things I've tried: If second part (being able to make an API request to the back-end) was not in the picture, then I managed to use angular APP_INITIALIZER to get things done, I then searched for some articles and found this one Managing dependencies among App Initializers in Angular, which has 3 approaches listed, 3rd one being the recommended one (as it is easy to maintain), but I don't really understand all of it, I believe author has not included full code implementation of each approach (I do get that it's authors call whether to provide code samples or not, and I might be wrong). I would really appreciate if anyone with experience could share their knowledge wrt the same.

PS: I haven't added any code in here as I am not really confident if what I have tried is sensible or not, but happy to add some code.

Stackblitz1 (single APP_INITIALIZER) - https://stackblitz.com/edit/angular-puaw7a

[The Problem] Stackblitz2 (multiple APP_INITIALIZER) - https://stackblitz.com/edit/angular-7uqijv

5 Answers 5

23

just use

useFactory: (appConfigSvc: ConfigService,settingsService:SettingsService) => {
        return () => {
          return appConfigSvc.loadConfig().then(()=>{
            return settingsService.loadConfig()
          });
        };
      }

See your forked code in stackblitz

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks Eliseo, this does the trick for my scenario. I still curious if it'd be possible to implement something similar to this post . That to me looks a bit more maintainable if need arises to add more services / factories.
This should be in the official documentation.
Thanks @eliseo, I lost 3+ hours just because I don't know how to do a proper search. This helps a lot.
I have tried to dockerize an angular APP for a while now and failed to set the graphql url runtime from docker env. I have tried reading config from json, js and ts files from the assets folder.. however the value was always initialized too late as i need it in another provider in the app to set up my graphql APOLLO client config. Thanks for sharing as this works! I am now updating the json config with an envsubst command from the entryPoint.sh script and as the APP_INITIALIZER provider step now waits until the value is set everything works!
2

I know that it's been while, but I have yet another solution. I made wrapper for initializer function that depends on config.

In app-module define providers like this:

providers: [
    { provide: APP_INITIALIZER, useFactory: initConfig, deps: [...], multi: true },
    { 
        provide: APP_INITIALIZER,
        useFactory: withConfig(initCrm, [initCrm_Deps]), // use wrapper 
        deps: [Injector], // Injector is required for withConfig
        multi: true
    }
]

Wrapper function:

export function withConfig(factory: Function, deps: any[]) {
    return (injector: Injector) => {
        return () => AppConfig.instance$
            .toPromise()
            .then(() => {
                // Inject dependencies
                const depsInstances = deps.map(d => injector.get(d));

                // Execute original function
                return factory.apply(globalThis, depsInstances)();
            });
    };
}

AppConfig.instance$ is a Subject which emits value after config is loaded.

Comments

1

I don't think you actually need an initializer in your case. You just have a value that other services depend on. The problem in your code is that you have an async value and try to expose it as a sync value.

I think your problems would be solved if you would just expose the config as an Observable and "await" it where it's needed. The benefits are that the application loads as much as it can until the config requests are done.

For example the shareReplay(1) operator will keep in memory the item and will defer the HTTP request until it's actually needed:

export class ConfigService {

  configData$ = this.httpClient.get<{config1:string}>('./assets/config.json').pipe(shareReplay(1));

  constructor(private httpClient: HttpClient) { }
}

Now your 2nd service can await the configData from the 1st service. Or just transform it via observable piping and expose it further as an observable to defer everything until it's actually needed.

@Injectable({
  providedIn: 'root'
})
export class SettingsService {

  settingsData$ = this.configService.configData$.pipe(
    map(c => c.config1 + '...and config service dependent action'),
    shareReplay(1), // also keep the value in memory maybe?
  );

  constructor(
    private httpClient: HttpClient,
    private configService: ConfigService
    ) { }
}
export class HelloComponent implements OnInit {

  @Input() name: string;

  dataFromConfigSvc: Observable<string>;
  constructor(private configService: ConfigService) { }

  ngOnInit() {
    // you could either use async pipe in the template or subscribe here and get the value
    this.dataFromConfigSvc = this.configService.configData$.pipe(map(c => c.config1));
  }

}

2 Comments

I think I'd need APP_INITIALIZER for at least first part (to be able to load application config), and second part (fetching settings from back end) could probably be postponed, but honestly in my case that really needs to take place as soon as the application loads. One thing though, it would make sense for application to fail to load if first part fails, I don't think the same needs to be done for the second part. I would still like to know how could I get multiple APP_INITIALIZERs to work when there is dependency involved.
Well, the flexibility of Observables is that you can can do what ever you want so you can also start loading config as soon as the service is constructed. For the app to fail to load, the bast place is an HTTP_INTERCEPTOR, or even a route guard. If want to do it with the APP_INITIALIZER just await the services init in order of the dependencies. ( await service1.init(); await service2.init(); )
1

To follow up on my comments on @Eliseo's post which worked out very well for me I also needed to load config for another provider step as I set up my GraphQL backend based on docker environments. Here is my code:

app.module.ts:

const appInitializerFn = (appConfig: AppConfigService) => {
  return () => {
    return appConfig.loadAppConfig();
  };
};

var graphqlUri: string = "";

...
providers: [
  AppConfigService,
  {
    provide: APP_INITIALIZER,
    useFactory: (appConfigService: AppConfigService) => {
      return () => {
        return appConfigService.loadAppConfig().then(() => {
          graphqlUri = appConfigService.getConfig().graphqlApiBaseUrl;
        });
      };
    },
    multi: true,
    deps: [AppConfigService]
  },
  {
    provide: LocationStrategy,
    useClass: HashLocationStrategy
  },
  {
    provide: APOLLO_OPTIONS,
    useFactory: (httpLink: HttpLink) => {
      return {
        cache: new InMemoryCache(),
        uri: graphqlUri,
        deps: [HttpLink]
      }
    } 
  }
]
...

Comments

0

@Eliseo's response worked well for me, though I had to change his stack blitz service code to be like this:

import {firstValueFrom} from 'rxjs';

async getData(): Promise<MyRecord> {
  return await firstValueFrom(this._httpClient.get<MyRecord>(...url...);
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.