AD8
AD8

Reputation: 2208

angular multiple APP_INITIALIZER that depend on each other

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

Upvotes: 15

Views: 15162

Answers (5)

Tomas Andersen
Tomas Andersen

Reputation: 329

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]
      }
    } 
  }
]
...

Upvotes: 1

Louis Cribbins
Louis Cribbins

Reputation: 189

@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...);
}

Upvotes: 0

Krzysztof
Krzysztof

Reputation: 16140

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.

Upvotes: 2

Eliseo
Eliseo

Reputation: 57941

just use

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

See your forked code in stackblitz

Upvotes: 23

Andrei Tătar
Andrei Tătar

Reputation: 8295

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));
  }

}

Upvotes: 1

Related Questions