DEV Community 👩‍💻👨‍💻

Cover image for Progressive Reactivity with NgRx/Store and NGXS
Mike Pearson for This is Angular

Posted on

Progressive Reactivity with NgRx/Store and NGXS

In this series I came up with 3 rules to achieve progressive reactivity. Following them reduced NgRx/Store and NGXS code by 18%. Here they are again:

  1. Keep code declarative by introducing reactivity instead of imperative code
  2. Don't write callback functions
  3. Wrap imperative APIs with declarative ones

Let's walk through each level of complexity and see how reactivity reduced the code, making the syntax more progressive as well.

Level 3: Complex Changes and Derived State

Here is the first level that benefits from selectors and Redux Devtools.

Unfortunately, the setup is the biggest jump in the amount of code for NgRx and NGXS. The non-template code jumps from 10 to 49 for NGXS and to 42 for NgRx/Store. A main reason for this was that in Level 2 we were just calling .next() on a BehaviorSubject from the template, but suddenly with NgRx and NGXS we need to dispatch actions to change anything.

Actions are normally dispatched from event handlers/callbacks, but this breaks Rule 2: Don't write callback functions. So, I wanted to find an alternative.

For NgRx, this was actually kind of easy. I just declared the store as public so I could do store.dispatch(actions.changeColor( from the template. However, this was ugly, and sort of broke the spirit of Rule 2, which is to keep event sources minimal. Also, NGXS actions are classes, which means they can't be new'ed from the template; so NGXS still needed methods. This was the reason for the extra 4 imperative statements it had above NgRx/Store.

A single changeColor function call from the template is ideal. So I created a utility that takes in an object of actions and returns an object of action dispatchers. For NgRx, I could just pass in the result of createActionGroup, which is an amazing function. For NGXS, I put all the actions in one file and imported it like this:

import * as actions from './actions.';
Enter fullscreen mode Exit fullscreen mode

Then I assigned a property on the component class with the result of my utility function:

  actions = createActionDispatchers(actions);
Enter fullscreen mode Exit fullscreen mode

How did I implement this function? I don't have that exact source code, because I have since modified it. But this is the relevant part of the function that I ended up using by the end:

  const store = inject(Store);
  // ...
  for (const actionName in actionGroup) {
    facade[actionName] = ((payload: any) =>
      store.dispatch(actionGroup[actionName](payload))) as any;
  }
Enter fullscreen mode Exit fullscreen mode

You can see the current, full implementations here:

Basically, I'm looping through each action in the object passed into the function and creating a function that dispatches the action to the store. Since I assigned it as a component class property, I can use every action directly there like this:

(colorChange)="actions.changeColor({newColor: $event, index: i})"
Enter fullscreen mode Exit fullscreen mode

This will take care of creating the action object/class and dispatching it to the store.

Oh, and a requirement for NGXS you need to keep in mind: Make sure the constructor takes only one parameter. There was no way around this for a reason I'll explain below, but it also made this part easier to implement.

At this point I had an idea. If I'm abstracting the interaction to the store behind this actions object, why don't I just do the same for selectors? We've got selectors, and every single one of them is going to end up needing this.store.select(...) to be called. We could save some code. And could I just put in on the same object and handle it in the same function? It would be easy to differentiate between actions and selectors: Actions are functions, selectors are observables with a $ at the end of their names.

For NgRx, this was easy. I just exported all the selectors from one file and imported them like import * as selectors from './selectors';. But NGXS couldn't be as simple, because selectors are defined as methods of classes, and some of them require an extra function call, so the treatment isn't uniform. So for NGXS, you need to define a new object for the selectors, such as this:

  selectors = {
    favoriteColors: FavoriteState.colors(),
    allAreBlack: ColorsState.allAreBlack,
  };
Enter fullscreen mode Exit fullscreen mode

This could just be a 2nd argument to our createActionDisptachers function, but that's not a good name anymore. I struggled to come up with a name, but I noticed that the returned object has the same basic shape as a facade in the facade pattern. It does not serve the same purpose as the facade, since the goal in reactivity is to make the event (action) as pure and close to the actual event source as possible, whereas facades provide an extra layer of decoupling you can freely add imperative commands to. If you are opposed to the direction I'm going in, you should go back and review Rule 2. With unidirectional/reactive code, the event source is simple: It just declares what happened. The flexibility is supposed to be downstream from that, not before it. So the philosophies might be different, but since the APIs they create are identical, I went ahead and called my function createReactiveFacade. I'll explain the reactive part later. It's really cool. And if you have an alternative name for this, please share.

createReactiveFacade's implementation is slightly different for NgRx and NGXS. In NgRx, we need to strip off the select, call toLowerCase() on the next character, and append a '$'. In NGXS we just need to append a '$'. But both return the same object, so the usage is identical:

  <app-color-picker
    *ngFor="let color of facade.colors$ | async; index as i"
    [color]="color.value"
    [colorName]="color.name"
    (colorChange)="facade.changeColor({newColor: $event, index: i})"
  ></app-color-picker>
Enter fullscreen mode Exit fullscreen mode

So, to sum up Level 3: Don't use methods to dispatch actions. Use this utility function instead. With less code, hopefully the work of moving from Level 2 to Level 3 doesn't involve too much refactoring.

Level 4: Reusable State Patterns

This is more about the "progressive" part of "progressive reactivity".

The motivation for progressive syntax is the impossibility of predicting all future user needs. Designs will evolve, and the code has to be able to evolve with them. High quality code is code that only requires small changes to be able to handle higher complexity. Poor quality code is limited to the current level of complexity. This is what I called a "syntactic dead end" in Part 1 of this series.

One form of complexity is having multiple versions of the same thing. Software is supposed to excel at handling this type of thing, but this is a problem with common state management patterns.

For example, you might have all your state management perfectly set up to handle a single datagrid on a page, but then users give feedback that they need to compare it side-by-side with a second one. The state management pattern will be the same; they will just have different actual state inside of them.

For NgRx/Store and NGXS, the first solution that usually comes to mind is the wrong one: Make our state more deeply nested by having a parent object like this:

interface ParentState {
  list1: ListState;
  list2: ListState;
}
Enter fullscreen mode Exit fullscreen mode

and then adding a property on every action so our reducers/handlers know which state to change.

Don't do this.

This pattern absorbs a state management problem into the state logic itself. It makes state changes harder to understand. It's also a pain to implement.

The best approach may not seem obvious, but you'll love it after you get used to it. It involves a little bit more work up front, but by the time you're finished it ends up being less work. The exact details differ between NgRx and NGXS.

NgRx/Store

For NgRx, let's say you have a reducer that's defined like normal. As an example, here's my Level 3 reducer in the colors app:

export const initialState = ['aqua', 'aqua', 'aqua'];

export const colorsReducer3 = createReducer(
  initialState,
  on(action, (state, { index, newColor }: ColorChange) =>
    state.map((color: string, i: number) => (i === index ? newColor : color))
  )
);
Enter fullscreen mode Exit fullscreen mode

To make multiple reducers with this same state pattern, just cut and paste every state change function outside the reducer and give it a name. Put all of it in a file and name it with a .adapter.ts extension, using NgRx/Entity's naming convention (a state adapter is really what we're creating). Then import it into the reducer file and use it as many times as needed:

// -.adapter.ts
export const changeColor = (
  state: string[],
  { index, newColor }: ColorChange
) => state.map((color: string, i: number) => (i === index ? newColor : color));

// -.reducer.ts

import { changeColor } from './4-state-adapters.adapter';

export const favoriteReducer = createReducer(
  ['aqua', 'aqua', 'aqua'],
  on(colorActions.changeFavoriteColor, changeColor)
);
export const dislikedReducer = createReducer(
  ['orange', 'orange', 'orange'],
  on(colorActions.changeDislikedColor, changeColor)
);
export const neutralReducer = createReducer(
  ['purple', 'purple', 'purple'],
  on(colorActions.changeNeutralColor, changeColor)
);

export const colorsReducer = combineReducers({
  favorite: favoriteReducer,
  disliked: dislikedReducer,
  neutral: neutralReducer,
});
Enter fullscreen mode Exit fullscreen mode

This might seem like more code initially, but if you feel up to it, go ahead and fork my StackBlitz and try implementing it the other way. It doesn't scale to higher complexity well. This way does. And it's much simpler migration work: Just a lot of copying and moving code around. The other way is riskier, since it modifies the state structure/logic itself. And by the end you'll see that it's a lot more code too.

For actions, the prop types can be extracted and reused, because each reducer needs its own version of the original action now. With createActionGroup, it's really easy:

export interface ColorChange {
  index: number;
  newColor: string;
}

export const colorActions = createActionGroup({
  source: 'Colors',
  events: {
    'Change Favorite Color': props<ColorChange>(),
    'Change Disliked Color': props<ColorChange>(),
    'Change Neutral Color': props<ColorChange>(),
  },
});
Enter fullscreen mode Exit fullscreen mode

An added benefit of this approach: Actions in Redux Devtools will have more specific labels.

For selectors, we want those in their own file still, but we will move our reusable selector logic to our .adapter.ts file and import it into our .selectors.ts file. So we used to have this:

export const selectColorsState = createFeatureSelector<string[]>('colors');

export const selectColors = createSelector(selectColorsState, (state) =>
  state.map((color) => ({
    value: color,
    name: color.charAt(0).toUpperCase() + color.slice(1),
  }))
);
Enter fullscreen mode Exit fullscreen mode

Now we have this:

// -.adapter.ts
// </state change functions>

// selector functions
export const getSelectColors = (getColors: (state: any) => string[]) =>
  createSelector(getColors, (state) =>
    state.map((color) => ({
      value: color,
      name: color.charAt(0).toUpperCase() + color.slice(1),
    }))
  );

// -.selectors.ts
import { getSelectColors } from './4-state-adapters.adapter';

// Feature selectors
export const selectFavorite = (state: any) => state.colors4.favorite as string[];
export const selectDisliked = (state: any) => state.colors4.disliked as string[];
export const selectNeutral = (state: any) => state.colors4.neutral as string[];

// Selectors reusing selector logic
export const selectFavoriteColors = getSelectColors(selectFavorite);
export const selectDislikedColors = getSelectColors(selectDisliked);
export const selectNeutralColors = getSelectColors(selectNeutral);
Enter fullscreen mode Exit fullscreen mode

Let me know if there's a more minimal way of doing this. I don't like this. But it would be worse if we had nested our state.

NGXS

I used to think it wasn't possible to take a normal NGXS state class and make it reusable. Then I got creative and found a really nice solution.

What you'll want to do is copy the original state class and paste it into a new file ending in .adapter.ts. Now, get rid of the @Action(SomeAction) decorators in that new file.

Now go to the original state class. Import and extend the class from the .adapter.ts file. Keep the individual lines where those decorators still are, and replace the action handler methods with property assignments from the parent class. So it will be like this:

@((Action as any)(FavoriteColorChange))
changeColor = super.changeColor;
Enter fullscreen mode Exit fullscreen mode

What's up with the Action as any? Well, decorators don't modify the type of the thing they're modifying, so this isn't much more dangerous than decorators in general. Without the as any, you'll get something about the decorator expecting the next thing to be a method implementation. But we're just getting the decorator to modify our own copy of the base class's action handler. Go check out the StackBlitz. It's working, so I am happy.

Now copy the actions into the .adapter.ts file, and remove the type properties from them. In the .actions.ts file, import those base classes without redefining a constructor, and extend them and add the type property, like this:

import { ColorChangeAction } from './4-state-adapters.adapter';

export class FavoriteColorChange extends ColorChangeAction {
  static readonly type = '[Colors] Change Favorite Color';
}
export class DislikedColorChange extends ColorChangeAction {
  static readonly type = '[Colors] Change Disliked Color';
}
export class NeutralColorChange extends ColorChangeAction {
  static readonly type = '[Colors] Change Neutral Color';
}
Enter fullscreen mode Exit fullscreen mode

Now these are the actual actions you can listen to in your new child state classes.

How about selectors?

This used to be how we defined our selectors:

  @Selector()
  static colors(state: string[]): Color[] {
    return state.map((color) => ({
      value: color,
      name: color.charAt(0).toUpperCase() + color.slice(1),
    }));
  }
Enter fullscreen mode Exit fullscreen mode

We can delete this from the child class, because it's now part of the base class. But we need to modify it so it works there. Turn it into a static method that returns a createSelector call:

  static colors() {
    return createSelector([this], (state: string[]): Color[] =>
      state.map((color) => ({
        value: color,
        name: color.charAt(0).toUpperCase() + color.slice(1),
      }))
    );
  }
Enter fullscreen mode Exit fullscreen mode

This adds a little bit of boilerplate, but it's straight-forward, so whatever.

We don't need to reference this at all in our state classes that extend this base class. But when we use the selector, it is very important to remember to invoke this static method in order to get the actual selector. TypeScript will not save you if you try to use this directly with the @Select decorator. And make sure you are getting it from the child class, not the base class. Anyway, here's an example of using this selector from each state class with createReactiveFacade:

  selectors = {
    favoriteColors: FavoriteState.colors(),
    dislikedColors: DislikedState.colors(),
    neutralColors: NeutralState.colors(),
  };
  facade = createReactiveFacade([actions, this.selectors], {});
Enter fullscreen mode Exit fullscreen mode

I am pretty happy about this. I thought it was impossible before, and it turned out to not even be that bad.


This was the section that was most different between NgRx/Store and NGXS. It should be easier from here on.

Level 5: Asynchronous Sources

NgRx/Effects is overrated. It seems reactive, but it isn't really. Everything that happens inside it determines the behavior of something somewhere else. This isn't declarative.

NGXS action handlers are similar to NgRx/Effects.

So, a long time ago I proposed a more reactive way to handle side-effects: Plain RxJS in a service. This post is already really long, so I don't want to go into the details, but it is much more reactive for many reasons you can read about here.

StateAdapt implements the method I described in that article internally, so you don't have to think about it. The result is extremely convenient syntax for reacting to state changes.

I wanted to bring what I could from StateAdapt's syntax to NgRx and NGXS. This is what the reactive part of createReactiveFacade refers to. I'll just show you how to use it, and describe its behavior, and if you're interested you can check it out on StackBlitz to see how it works.

Demos of NgRx/Store data fetching commonly go like this: The component is smart enough to know that it can't just subscribe to facade.data$ and expect to get what it asked for; it also needs to call facade.fetchData. That method knows it needs to dispatch an action called FetchData. Inside NgRx/Effects, you listen to FetchData, call the API, and return a new action DataReceived containing the data. Now the reducer can react to that last action.

That's 3 imperative statements. In StateAdapt it takes 0. But the best we can do in NgRx/Store and NGXS is going to be 1. Here's what it looks like:

  favoriteColors$ = timer(3000).pipe(
    map(() => ({ colors: ['aqua', 'aqua', 'aqua'] }))
  );

  facade = createReactiveFacade([colorActions, selectors], {
    favoriteReceived: this.favoriteColors$,
  });
Enter fullscreen mode Exit fullscreen mode

Before I explain why I considered this imperative, I'll explain what's going on from top to bottom.

favoriteColors$ is like the observable of the data from the server, something like what http.get would return.

createReactiveFacade takes a 2nd argument that's an object with keys named after actions and values that are observables of the payload/props of the action named in the key, which will be dispatched whenever the observable emits. In this example, after 3 seconds favoriteColors$ will emit, and this will trigger facade.favoriteReceived to be called, which will dispatch that action.

Additionally, the HTTP request will not be sent off until something subscribes to one of the selectors inside the facade object. This is why it's more reactive than the common approach with NgRx/Effects of NGXS action handlers. This means if something unsubscribes, the HTTP request will be canceled, as you'd expect if you were dealing with the HTTP observable directly.

But it's not totally reactive, because it's defining where an action is getting its data from in a place completely different from either the action's declaration or the reducer/state whose behavior it eventually determines. Every time an action is dispatched in NgRx and NGXS, something imperative has occurred, because of this scattered/non-declarative code organization. That's why the best NgRx/Store and NGXS can do is 7 imperative statements, while the class-based libraries and StateAdapt can reach the minimum of 4, with help. In other words, NgRx/Store and NGXS are the least unidirectional (reactive) state management libraries for Angular. But, other than StateAdapt, they're also the only ones that support both selectors and Redux Devtools, so that's why we need them.

There's one important limitation with NGXS I'll repeat: Your action constructors can only have one argument, because the observables will be emitting one value and it's not possible to spread it onto class constructors.

Level 6: Multi-Store DOM Events

This is going to be very easy. NgRx/Store, NGXS, RxAngular and StateAdapt all can respond to shared event sources reactively. For NGXS and NgRx, you just dispatch an action and listen to it in multiple places. For RxAngular and StateAdapt, you define a single Subject or Source and connect it to multiple stores. When you push to it (unavoidable imperative statement), your stores will react.

If you're wondering what a 100% reactive DOM library looks like, check out CycleJS. It's very interesting. Instead of defining an action or Subject that you push to from the DOM, you declare an event source as originating from the DOM itself.

Level 7: Multi-Store Selectors

This is another thing that NgRx/Store and NGXS easily support.

For NgRx/Store, you just pass selectors from any store you want into createSelector.

For NGXS, it's more complicated. Normally, you define a service that just serves as a container for your "meta selector". But I defined it as part of the parent state class for my 3 color states, since that class had to exist anyway. (I really tried to implement things in the most minimal way possible to shine the most positive light possible on every library.) Anyway, you can read about meta selectors here, but this is how it looked in my colors app:

@State<string[]>({
  name: 'colors',
  children: [FavoriteState, DislikedState, NeutralState],
})
@Injectable()
export class ColorsState {
  @Selector([
    FavoriteState.allAreBlack(),
    DislikedState.allAreBlack(),
    NeutralState.allAreBlack(),
  ])
  static allAreBlack(state: any, ...results: boolean[]) {
    return results.every((a) => a);
  }
}
Enter fullscreen mode Exit fullscreen mode

And then I used it like this:

  selectors = {
    favoriteColors: FavoriteState.colors(),
    // ...
    allAreBlack: ColorsState.allAreBlack,
  };
  facade = createReactiveFacade([actions, this.selectors], {
  // ...
Enter fullscreen mode Exit fullscreen mode

And in the template it became available as facade.allAreBlack$.

And that's it!

Conclusion

I'm pleasantly surprised at how easy this was compared to how I thought it would be. NgRx/Store stayed at 7 imperative statements, and NGXS went from 11 to 7. NgRx went from 218 to 178 lines of code, and NGXS went from 251 to 207 lines of code.

For my next article, I'm going to try to fit Subjects in a Service, Akita, Elf, RxAngular and NgRx/Component-Store all in the same article. They are very similar, so it makes sense to cover them together.


There was a lot more to explain than I remembered. If you're interested in watching me struggle through this stuff in realtime, I recorded it and uploaded it to YouTube, but the NgRx video is scheduled to release on August 25, 2022 and the NGXS video will be August 30th, 2022 (I didn't want to flood subscribers with all the videos I was recording everyday). Actually, these videos are just the explanations of createReactiveFacade. Other videos on my channel already published are of me doing all the StackBlitz work for this article series. It won't be fun to watch, but someone might be interested.

Top comments (0)

Thank you.

 
Thanks for visiting DEV, we’ve worked really hard to cultivate this great community and would love to have you join us. If you’d like to create an account, you can sign up here.