Reputation: 933
I just discovered a bug in our software - pretty sure it used to work, but now it's broken.
Plunkr reproducing the issue can be found here: https://embed.plnkr.co/BNRcQFWGkkEitxjwE7pi/
In our app.module we have set up providers for APP_INITIALIZER, APP_BASE_HREF and ErrorHandler in three different factories
AppInitializerFactory
Calls an async load method for our config, which is stored in a json file. In this sample loads baseHref and submitToSentry
ErrorHandler
A custom errorhandler that submits to Sentry, if config tells it to. In constructor config is not loaded yet - and that is acceptable. Something about having to setup ErrorHandler or APP_INITIALIZER first. They had to pick one. Instead we look at config once an error occurs - and if we should submitToSentry - we do.
BaseHrefFactory
Was intended to set the APP_BASE_HREF injection token to whatever baseHref in Config was. As the Plunkr shows, baseHref printed ends up being undefined - not myBaseHref as expected from config.json.
If... I comment out the AppRoutingModule in app.module.ts:52 - then it works.
Question is... (As I need the AppRoutingModule)
I guess I'm once again (as with the errorhandler) is in a race condition between setting up routing and initializing the app - And somewhere internally in @angular/router/routerModule they inject APP_BASE_HREF.
Are there any way to achieve that APP_INITIALIZER holds back routing module until done?
Am I just holding it wrong? Is there something in the ordering or similar that can fix this.
Hoping for someone to be able to explain me - have used quite a few hours debugging it myself - and is currently lost.
Glad though that the Plunkr could reproduce it.
Upvotes: 4
Views: 2104
Reputation: 27526
[Too late now I'm sure, but I'll have a go at answering this. Note that although the plunkr still shows the code, it doesn't actually run - like most plunkr examples on SO these days]
Your provider setup has: provide: APP_INITIALIZER, useFactory: AppInitializerFactory, deps: ConfigServiceBase
and although it's not obvious from the function declaration (since you're not being explicit about the type of the return value), AppInitializerFactory
returns a function that returns a promise. This is important, since the Angular initialization process will wait for that promise to be resolved before continuing to load the rest of the app (loading the initial component, etc).
You also have provide: APP_BASE_HREF, useFactory: BaseHrefFactory, deps: ConfigServiceBase
. But, since this is not associated with APP_INITIALIZER
, Angular makes no particular guarantee as to when the BaseHrefFactory
will be executed (though it's unlikely to be executed until something asks for the APP_BASE_HREF
). The fact that it sometimes gets executed after AppInitializerFactory
may be pure luck.
To be clear, providers using the APP_INITIALIZER
token are guaranteed to be instantiated before the first component is created - but this is unrelated to the order in which services are instantiated. The dependency between services is expressed via the dependency-injection tree.
Note that the Angular router makes use of APP_INITIALIZER
itself - some of the initialization of the router happens there. So, it's possible that by adding routing into the mix you change the (already random) order in which things happen.
Another thing to bear in mind is that, although BaseHrefFactory
has a dependency on ConfigServiceBase
, it does not have a dependency on "an instance of ConfigService
whose load()
method has been called". The injector will quite happily pass it a reference to an instance of ConfigService
whose load()
method has not been called.
To solve all this, I would inject a service rather than try to inject a value, as follows.
1) Create a service to hold the value:
@Injectable()
export class AppBaseHrefService {
public baseHref: string;
}
2) Modify the AppInitializerFactory
function to take an AppBaseHrefService
parameter, and add an extra step to set the value based on the config:
export function AppInitializerFactory(config: ConfigServiceBase, appBaseHrefService: AppBaseHrefService) {
return () => config.load()
.then(() => {
appBaseHrefService.baseHref = config.baseHref
});
}
3) Give your APP_INITIALIZER
provider a dependency on AppBaseHrefService
:
{ provide: APP_INITIALIZER, useFactory: AppInitializerFactory, deps: [ConfigServiceBase, AppBaseHrefService], multi: true }
Of course, you may decide that it's easier to simply inject the existing ConfigService
/ ConfigServiceBase
rather than injecting yet another service. I'd be inclined to agree, unless your actual ConfigService
has lots of configuration information and you only want to expose some of it.
UPDATE July 2019, since no-one seems to be able to get this to work...
Firstly, please bear in mind that I'm not claiming to be able to set the value provided by APP_BASE_HREF
. Instead, I'm suggesting that you could inject something else that will provide the value you need.
I've created a StackBlitz that shows this in action. Here I have the following moving parts:
I have an InitializerProvider
provider defined in the AppModule
and associated with the APP_INITIALIZER
DI token. This uses a factory function to do asyncronous initialization. Because that function returns a Promise
, no components will be created etc. until that promise has been resolved (i.e. initialization is complete).
The factory function above uses the ConfigService
to asynchronously fetch "some data". It then uses that data to initialize the BaseUrlService
. More synchronous or asynchronous actions could be added here as necessary. (Async/await is great for this stuff).
Components (e.g. AppComponent
in this example) can inject BaseUrlService
and use that to access the base URL that's derived from the data fetched during initialization. Since the initialization happens before any components are created, there should be no circumstances in which those components see an un-initialized service.
HTH.
Upvotes: 3