1

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.

António Quadrado
  • 1,307
  • 2
  • 17
  • 34
  • You can try to show the modal in the component instead of the store. The component will have ChangeDetectorRef available. In components I usually use: ɵdetectChanges. Found in this answer: https://stackoverflow.com/questions/58784993/what-does-angular-ivy-specifically-allow-us-to-do-in-regards-to-manual-change-de – Eli Nov 26 '20 at 09:55

2 Answers2

1

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.
kremerd
  • 1,496
  • 15
  • 24
0

Try returning the observable instead of subscribing to it. This way NGXS will take care of the subscription for you and trigger a change detection. See the snippet below:

@Action(SomeAction)
  public someAction(ctx: StateContext<{}>): void {
    return timer(2000) // returns the observable in the action handler 
      .pipe(
        first(), // completes after one emission
        mergeMap(() => {
          throw new Error("Some error");
        })
      )
      .pipe(
        catchError(error => {
          this.confimationService.triggerConfirmation();
          return of(undefined);
        })
      )
  }

Returning the observable to the action handler binds the observable to the lifecycle of the action in NGXS. Just be sure this Observable completes eventually otherwise your action will also never complete.

  • Did you try that? Because I had tried that before and it doesn't work. I've updated the stackblitz if you want to check it. – António Quadrado Nov 26 '20 at 12:08
  • `timer` is observable that never completes. You should not have this kind of asynchronous action in your action. I've noted this in my answer. Here it works fine: https://stackblitz.com/edit/angular-ivy-yq3j7m?file=src%2Fapp%2Fstate%2Fmain.store.ts – Mateus Carniatto Nov 26 '20 at 12:57
  • I don't understand, this stackblitz has the same as mine and in my really case I am not using `timer` I'm using a call to an api so it will be asynchronous... not sure I understood your point. – António Quadrado Nov 26 '20 at 14:08