This series examines how we can maintain declarative code as our features scale through increasing levels of complexity.

Level 6: Multi-Store DOM Events

There are cases where a single DOM event must be observed by more than one store. Imagine a button in our template that resets every store's colors to black. If Rule 2 didn't apply, a click handler might look like this:

  setAllToBlack() {
    // `set` is a state change that should come by default with every adapter
    this.favoriteStore.set(['black', 'black', 'black']);
    this.dislikedStore.set(['black', 'black', 'black']);
    this.neutralStore.set(['black', 'black', 'black']);
}
Enter fullscreen mode Exit fullscreen mode

Writing callback functions that fan out multiple actions from one event is a common pattern. That approach is imperative and often leads to missed updates and inconsistent state. We now have four imperative statements instead of the single one from the template, and Devtools shows three "events" dispatched in rapid succession, obscuring the root cause of those changes.

If you work with NgRx or NGXS, stay reactive by dispatching exactly one action per event, and let all reducers, states, or stores respond to that one action. This preserves the declarative nature of both event sources and stores while cutting down on repetition.

Let's incorporate this state transition into the adapter:

    setAllToBlack: state => ['black', 'black', 'black'],
Enter fullscreen mode Exit fullscreen mode

The button should push as little data as possible to a single TypeScript location. Since three stores need that data, we need an isolated place for the event to land, and then all stores can react to that one spot. Annotating the event source is also desirable. Something along these lines works:

  blackout$ = new Source('[Colors] Blackout');
Enter fullscreen mode Exit fullscreen mode

Every store can subscribe to this source and update its state like this:

    setAllToBlack: this.blackout$,
Enter fullscreen mode Exit fullscreen mode

Here's the complete implementation with all the changes called out:

export class ColorsComponent {
  adapter = createAdapter<string[]>({ // For type inference
    changeColor: (colors, [newColor, index]: [string, number]) =>
      colors.map((color, i) => i === index ? newColor : color),
+   setAllToBlack: state => ['black', 'black', 'black'],
    selectors: {
      colors: state => state.map(color => ({
        value: color,
        name: color.charAt(0).toUpperCase() + color.slice(1),
      })),
    },
  });

  initialState = ['loading', 'loading', 'loading'];
+
+ blackout$ = new Source<void>('[Colors] Blackout');

  favoriteColors$ = this.colorService.fetch('favorite').pipe(
    toSource('[Favorite Colors] Received'),
  );
  favoriteStore = createStore(
    ['colors.favorite', this.initialState, this.adapter], {
    set: this.favoriteColors$,
+   setAllToBlack: this.blackout$,
  });

  dislikedColors$ = this.colorService.fetch('disliked').pipe(
    toSource('[Disliked Colors] Received'),
  );
  dislikedStore = createStore(
    ['colors.disliked', this.initialState, this.adapter], {
    set: this.dislikedColors$,
+   setAllToBlack: this.blackout$,
  });

  neutralColors$ = this.colorService.fetch('neutral').pipe(
    toSource('[Neutral Colors] Received'),
  );
  neutralStore = createStore(
    ['colors.neutral', this.initialState, this.adapter], {
    set: this.neutralColors$,
+   setAllToBlack: this.blackout$,
  });
}
Enter fullscreen mode Exit fullscreen mode

And here's how it appears:

Color Picker—Multi-Store Dom Events

StackBlitz