Better Action Hygiene with Events in NgRx

We explore how a small adjustment can guarantee Good Action Hygiene, lead to extensible reducers and lower the high code-cost of NgRx.

Better Action Hygiene with Events in NgRx

NgRx can be a lot to digest for most Angular developers. Specially, for those of us from OOP backgrounds, the Redux pattern is a radical departure from the style of programming we are used to. Because of this, there have been few attempts to wrap NgRx into more Angular-like APIs or to provide alternate libraries altogether. The Facade pattern is an attempt at the former while Akita and NgXs are examples of the latter.

When our team was looking into NgRx, there was initially a strong preference in adopting the Facade Pattern to hide the boilerplate and the mystery of working with NgRx from the rest of the team. The original form of the pattern, however, was not recommended by the creators of NgRx for breaking Good Action Hygiene (GAH?).

Sam Julian wrote a good article weighing in the pros and cons and provided an alternate implementation. This looked more of a compromise than an alternative, as you would still write many Actions and Selectors.

In this post we are going to walkthrough the implementation of a few simple wrappers around NgRx, that will;

  1. Make the Reducers adhere to Open/Closed principle
  2. Strictly enforce “thinking of Actions as Events”
  3. Reduce the number of Actions needed while avoiding Action reuse and therefore,
  4. Allow us to use Facades without breaking GAH.

Good Action Hygiene

Here are the key points:

  1. Don't reuse Actions - Instead dispatch distinct Actions from each source. Takeaway - Treat Actions as unique events in the system, not commands.
  2. Avoid generic Action types - Similar to #1: By using distinct Action names, you can figure out where each Action is being dispatched from by looking at the Redux Dev Tools or just by going through the reducers. Takeaway - Keep your Actions traceable through source code and Store Dev Tools.
  3. Avoid Action sub-typing - Action sub-typing happens when you create a generic Action type and add an additional property (e.g. kind) to carry the specifics of how that action needs to be handled. The point here is that you'd end up writing a lot of nested conditionals all over the app to check these specifics. Takeaway - Use narrow Action types.

If you followed along closely, you'd see that #1 and #2 are the rules that the original Facade pattern broke.

But, while these guidelines make a lot of sense, there is still a problem:

Good Action Hygiene may not be SOLID

Let's take the example of adding Tacos or Burgers in the food ordering app from Mike Ryan's example on GAH. There are two actions, for the two pages from where you can order them:

[Menu Page] Add Taco
[Taco Details Page] Add Taco

The consequence of this is that your reducer now has to know about both of these distinct actions.

@Injectable()
class TacoEffects {
  @TacoEffects() addOne$ = this.actions$.pipe(
    ofType(
      '[Menu Page] Add Taco',
      '[Taco Detail Page] Add Taco'
    ),
    mergeMap(action => 
      this.tacoService.addOne(action.taco).pipe(...))
    )
}

As this example itself shows, the effect of the Action rarely cares about the source of the Action: It has the same responsibility regardless.

Now consider a situation where you run a banner ad on the homepage for Tacos. Now you have a new Action: [Taco Ad] Add Taco

Now you need to update your Reducers or Effects to tell them about the new Action. Obviously, this takes minimal effort. However, we are violating the Open/Closed Principle. If you follow the best practices of naming Actions with source, you cannot keep your Effects and Reducers closed from modification.

This is easy to fix.

Call an Event an Event

The first step is being honest about our Actions. The moment seasoned OOP'ers hear “Action” their minds race towards Commands. This is what Good Action Hygiene (GAH?) was trying to address. It asks us to think of Actions as Events. Let's take it a step further. Let's just make them Events.

export interface Event extends Action {
  readonly verb: string;
  readonly source: string;
  [other: string]: any;
}

The Action’s type has been split into a source and a verb. The verb is the actual event such as the “adding of a Taco”. The source is where the event originated.

Wait! Isn't this "Action sub-typing" which we are supposed to avoid? Not exactly. What we need is to avoid sub-typing actions in a way that you end up percolating conditional checks everywhere in the code; especially in reducers. As you are going to see, that is not going to happen with this approach.

Event Creators

Now, we'd want a creator for these events. Remember that we are deriving our Event type from Action and want them to look like a regular Actions to the rest of NgRx.

export function createEvent<P>(
    source: string,
    verb: string,
    config?: P
  ) {
    if (!config) {
        return () => ({ verb, source, type: `[${source}] ${verb}` })
    }
  
    return (prop: P) => ({
      ...prop,
      verb,
      source,
      type: `[${source}] ${verb}`
    });
  }

While we are at it, how about a prepareEvent method to get a "prepared event" that can simply be invoked with a source and parameters, without restating the verb each time?

export function prepareEvent(verb: string);
export function prepareEvent<ArgsType>(verb: string, config: ArgsType);
export function prepareEvent<ArgsType>(
    verb: string,
    config?: ArgsType
){
    if (!config) {
        const assembler = (source: string) => toEvent(source, verb);
        ((assembler as any) as VerbedEvent).verb = verb;
        return assembler;
    } else {
        const assembler = (source: string, prop: ArgsType) => ({
            ...prop,
            verb,
            source,
            type: `[${source}] ${verb}`,
        });
        ((assembler as any) as VerbedEvent).verb = verb;
        return assembler;
    }
}

Reducing

Now, we’d ideally want to write our reducer like this:

export const ordersReducer = createEventReducer(
    initialState,
    on(OrderEvents.tacoAdded, 
      (state, event) => /* ... */),
    on(OrderEvents.burgerAdded, 
      (state, event) => /* ... */)
  );

To achieve this we define our own onType and on(...) functions as:

export interface On<StateType> {
  reducer: ActionReducer<StateType, Event>;
  verb: VerbType;
}

export function on<StateType>(
  verb: VerbType,
  reducer: ActionReducer<StateType>
): On<StateType, VerbType> {
  return { verb, reducer };
}

export function createEventReducer<StateType>(
  initialState: StateType,
  ...ons: On<StateType>[]
): ActionReducer<StateType, Event> {
  const map = new Map<
    VerbType,
    ActionReducer<StateType, Event>
  >();
  for (let on in ons) {
    map.set(ons[on].verb, ons[on].reducer);
  }

  return function(
    state: StateType = initialState,
    action: Event
  ): StateType {
    const reducer = map.get(action.verb);
    return reducer ? reducer(state, action) : state;
  };
}

Dispatching Events

By now, you have everything you need to dispatch events and process them. This is what dispatching an event would look like.

const tacoAddedFromMenu = createEvent(
  'Menu Page', 
  OrderEvents.tacoAdded, 
  props<{ taco: Taco }>
); 
// NOTE: We do need a custom props function and a Props type here.
// See the sample or the linked gist at the end.

/* ... */

this.store.dispatch(tacoAddedFromMenu({ taco }));

Or, with prepared events:

const tacoAdded = prepareEvent(
  OrderEvents.tacoAdded, 
  props<{ taco: Taco }>
); 
// NOTE: We do need a custom props function and a Props type here.
// You can find the code for them in the attached gist or the sample repo.

/* ...  */

this.store.dispatch(tacoAdded('Menu Page', { taco }));

/* ... */

this.store.dispatch(tacoAdded('Taco Detail Page', { taco }));

Effects

In order to support Effects, lets add a simple event filter called onEvent:

export function onEvent(
  expectedEvent: VerbType
): OperatorFunction<Action, Event> {
  return flatMap((action: Action) =>
    (action as Event).verb === expectedEvent
      ? of(action as Event)
      : EMPTY
  );
}

Our example from earlier, now looks like this:

@Injectable()
class TacoEffects {
  @TacoEffects() addOne$ = this.actions$.pipe(
    onEvent(OrderEvents.tacoAdded),
    mergeMap(
      action => 
        this.tacoService.addOne(action.taco).pipe(/* ... */))
    )
}

The Event Store

While it's not strictly necessary, you could also override the NgRx Store and add a few methods to simplify event dispatching:

public dispatch(event: Action): void;
  public dispatch<VerbType extends string>(
    source: string,
    verb: VerbType
  ): void;
  public dispatch<VerbType extends string>(
    source: string,
    verb: VerbType,
    args: any
  ): void;

To Facade or Not?

If you really like the Facade Pattern, now you can implement the same without falling into the pitfall of reusing Actions [2]. Simply use prepared events and expose methods to raise each event.

@Injectable({ providedIn: 'root' })
export class OrderingFacade {

  /** selectors... */

  private readonly tacoAdded = prepareEvent(
      OrderEvents.tacoAdded, 
      props<{ taco: Taco }>
    ); 
  private readonly burgerAdded = prepareEvent(
      OrderEvents.burgerAdded, 
      props<{ burger: Burger }>
    ); 

  constructor(private store: EventStore<OrderState>)

  public tacoAddedFrom(source: string, taco: Taco) {
      this.store.dispatch(this.tacoAdded(source, { taco }));
  }

  public burgerAddedFrom(source: string, burger: Burger) {
      this.store.dispatch(this.burgerAdded(source, { burger }));
  }
}

Sample Project

To illustrate the use of the pattern, I have modified the NG Conf 2020 Workshop Sample by the NgRx Team. The modifications are on the complete branch of a fork from the original repo.

Conclusion

As you can see, all we really did was split up Action.type, into distinct source and verb properties. Everything else was about adding the code to support that pattern. There’s obviously no breakthrough innovation here; just a simple adjustment.

But, what that does is switch our mental model to actually treat Actions as Events like they were meant to. We already know that events, even similar in nature, represent different flows in the system when raised from different sources. We no longer need to learn that we shouldn’t reuse them.

We have also closed our Reducers and Effects from modification while keeping the application open for extension. The Reducers and Effects now need to react only to a specific type of event, and not be bound by the origin of the event. We achieved that without percolating any additional checks in our code.

You can find the full source of the implementation in this gist or the sample [1].

Notes

[1] You'll notice in the Gist that I've renamed on as when and props as args. This was done to avoid confusion with the built-in NgRx operations. I opted to keep them in their original names to make the code feel familiar to NgRx pros, while highlighting the key points.

[2] The Facade pattern for NgRx has also been criticised for the potential for “Selector Abuse”. We’ve not addressed that here. In general, steer your team clear of the Facade Pattern unless you are vigilantly reviewing code to catch such abuse.