Warm tip: This article is reproduced from serverfault.com, please click

NGXS How to trigger Angular re-render after asynchronous call?

发布于 2020-11-20 10:47:06

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.

Questioner
António Quadrado
Viewed
0
kremerd 2020-11-28 07:51:07

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:

  • If your action is synchronous, and the code that dispatched it runs in the NgZone, then the change detection cycle triggered at the end of the current (micro-)task will run after the side effect.
  • If you subscribe to the 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:

  1. While you can't inject a 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.
  2. If you want to leverage zone.js, you can also inject 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.
  3. If you want to run all your code in the NgZone, you can set 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.