I am doing some api calls from my store and I have a catch error that triggers a modal with the error message when an error is thrown. The problem is that when this happens the method to trigger the modal is called but the html is not rendered until I click somewhere on the page. This only happens from within the store, I have simulated it over several parts of the app like this:
timer(5000)
.pipe(
mergeMap(() => {
throw new Error('Some error');
}),
)
.pipe(
catchError((error) => {
return this.handleError(_(`Couldn't do the thing`))(error);
}),
)
.subscribe((result) => {
console.log(result);
});
I thought that I could inject the ChangeDetectorRef
to trigger manual re-render of html but I got NullInjectorError: No provider for ChangeDetectorRef!
and I can't make it work. My question is:
Is it possible to inject the ChangeDetectorRef
in the store and would it solve my problem? Also, as a follow up question, is any other way to circumvent this issue? According to some things I have been reading it seems to happen due to the store being outside of Angular scope so it can't know that needs to re-render something.
Any help would be much appreciated.
UPDATE: Here is a stackblitz illustrating the problem and a possible solution by dispatching an action to display the error message.
Usually change detection is triggered automatically by zone.js, more concretely after each (micro-)task that was registered in the NgZone.
NGXS by default does not run action handlers in the NgZone. That's a useful performance optimization and in most cases you won't notice the difference. In particular that's true when the action handler only modifies the actual state and doesn't have side effects. But in your case the action handler has a side effect: this.handleError(_("Couldn't do the thing"))(error)
, or confimationService.triggerConfirmation()
in the StackBlitz. And that side effect even reflects in the view.
Now, there are still a lot of ways how this could work out. All you need is a single change detection cycle triggered after the side effect. And that's where it gets really interesting: While the action handlers themselves don't run in the NgZone, there's a lot of surrounding code running in the NgZone. And that might indeed trigger the mentioned change detection cycle. In particular:
Observable
returned from store.dispatch
, then that subscription will emit and complete inside the NgZone. This therefore triggers two change detection cycles after the side effect.(Btw, the latter is the reason why dispatching two nested actions works in your Stackblitz: You subscribe to the dispatch method there!)
If you'd like to investigate the order of events for yourself, check out this Stackblitz. The console output should tell you pretty accurately what's going on in each of the scenarios.
Finally, let's talk about how you can ensure change detection is triggered correctly. There actually are a couple of options to choose from:
ChangeDetectorRef
in a state, you can inject an ApplicationRef
. Invoking its tick
method will asynchronously trigger a change detection cycle at the end of the current (micro-)task.NgZone
and use its run
method to run your whole side effect inside the NgZone. This has the added advantage that (micro-)tasks registered by your side effect will also be followed by change detection cycles.executionStrategy: NoopNgxsExecutionStrategy
in your NgxsConfig
. This will override the default behavior of NGXS and globally cause all action handlers to be run in the NgZone.