2

After tracking down unexpected requests being made I've found that my app's global user state's slices emit upon every navigation.

What are potential causes of an NGXS state slice

  1. emitting where its data hasn't changed and
  2. actions have not been dispatched

Here is the chronological order of events in the app

  1. OnInit
  2. slice emits -> fetch with params <-- EXPECTED
  3. user clicks to navigate to a new page
  4. UserState selectors emit -> fetch with params <-- UNEXPECTED
  5. OnDestroy

Obviously we don't want slices to emit for no changes in data.

#4 demonstrates a side-effect where an unnecessary request is triggered.

Here is the code

  @Selector()
  public static scope(state: DataModel) {
    console.log('scope triggered');
    return state.scope;
  }

  // Actions only trigger once as expected
  @Action(GetScopes)
  getAvailableScopes(ctx: StateContext<DataModel>) {
    return this.apiService.get('/scopes').pipe(
      tap((result) => {
        ctx.setState(
          patch<DataModel>({
            scope: result.scopes,
          })
        );
      })
    );
  }

The app is not complex but I cannot replicate unexpected slices emitting in StackBlitz Github issue

It should be noted that the state does not change!

ngxsOnChanges(change: NgxsSimpleChange) { ... only fires when the app inits and is never again triggered within in this flow despite the state's selectors emitting on navigation.

Ben Racicot
  • 5,332
  • 12
  • 66
  • 130

1 Answers1

3

The answer to the issue above is that state management has several weaknesses which your architecture will have to monitor.

One of them being that new subscriptions to selectors cause the selector to emit.

Navigating to a new route in the app can load another feature module. That feature module subscribes to the same Selectors causing the old subscriptions to emit despite the fact that they are about to be unsubscribed.

We solved this by setting up a resolver to ensure that all global states have been fetched and are available to every component. Then these globally shared states don't have the timing issues and can be used without subscribing to them.

resolver

@Injectable({ providedIn: 'root' })
export class AppShellResolver implements Resolve<unknown> {
  constructor(private store: Store) {}
  resolve(route: ActivatedRouteSnapshot): Observable<any> {
    return this.store
      .dispatch([
        new GetAvailableScopes(),
        ... <-- other required states for UI
      ])
      ...
  }
}

usage

this.scopes = this.store.selectSnapshot(UserState.scopes);
Ben Racicot
  • 5,332
  • 12
  • 66
  • 130
  • "One of them being that new subscriptions to selectors cause the selector to emit." I think this is normal behavior, what is the problem exactly with it? – Amer Jun 13 '22 at 11:25
  • The side effects of an entire state emitting must be accounted for in your architecture. The spirit of state management is that we can rely on sliced data to emit only when changed. This reliability goes out the window with the side effect I describe. – Ben Racicot Jun 13 '22 at 21:06
  • The state slice will be sent only to the new subscription, and won't be emitted again to the old subscription until the state changes. – Amer Jun 14 '22 at 04:40
  • That's the problem here, the old subs are emitting. Do you know of reasons why this is happening? Or perhaps even a way to stop this? – Ben Racicot Jun 15 '22 at 15:53
  • Do you have any other actions that you dispatch on navigation? – Amer Jun 15 '22 at 16:36
  • No actions are triggered and no states are changed. Like the question states `ngxsOnChanges` demonstrates that state is unchanged when these selectors emit. – Ben Racicot Jun 15 '22 at 18:23