DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Multi-Store DOM Events (Angular)
Mike Pearson for This is Angular

Posted on

Multi-Store DOM Events (Angular)

This series explores how we can keep our code declarative as we adapt our features to progressively higher levels of complexity.

Level 6: Multi-Store DOM Events

Sometimes multiple stores need to react to a single DOM event. Let's say we have a button in our template that sets all colors in all stores to black. If it weren't for Rule 2, we might write a click handler 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

It is very common for developers to write callback functions so they can dispatch multiple actions for a single event. This is the imperative style and unsurprisingly is often accompanied by forgotten updates and inconsistent state. The total number of imperative statements is 4 now, instead of just the 1 from the template, and we now see 3 "events" in Devtools dispatched back-to-back, which makes it harder to understand what actually happened that caused these changes.

If you are using NgRx or NGXS, keep your code reactive and dispatch exactly 1 action for each event, and have all reducers/states/stores react to that single action. This keeps our event sources and stores as declarative as possible, and reduces repetition.

So let's add this state change to the adapter:

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

Our goal with the button is to push the least amount of data possible to a single place in TypeScript. Since 3 stores need the data, we need to create an independent place to push the event to, and have all stores react to that. We also want to annotate that event source. So let's have something like

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

All the stores can connect this source and state change like this:

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

Here's the whole thing with these changes highlighted:

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

Here's how it looks:

Color Pickerβ€”Multi-Store Dom Events

StackBlitz

Top comments (0)

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.