NgRx is great, but the amount of boilerplate can be overwhelming. In this post I show two packages that can help drastically reduce the amount of boilerplate you have to write. Plus, there are other benefits for better type checking, and more efficient immutability.

NgRx actions, in their most basic form, can just be a simple POJO (plain old JavaScript object) that conforms to the following interface:

interface Action {  
  type: string;
}

We can benefit from strong typing by using an enum of types, defining Action classes, and a type union to combine the action class types. But, this introduces a lot of extra boilerplate code.

For the reducer, we have to ensure the state is immutable. We introduce a lot of extra boilerplate code again, to deal with immutability. Either by using the spread ... operator or the popular Immutable.js library.

There are two open-source projects that can help drastically reduce the amount of boilerplate we need, while still benefiting from strong typing and immutability.

  1. Unionize: described as “Boilerplate-free functional sum types in TypeScript”. It simplifies the creation of type-safe actions, and has helpers for creating reducers and working with actions.
  2. Immer: Applies some magic using JavaScript proxies to generate the next immutable state using a draft copy of the current state. This library is surging in popularity because it solves the immutability problem in a unique way that feels like native JavaScript. It simplifies the reducer logic because immutable code can be written using standard mutating JavaScript that everyone is familiar with. For more information on Immer check the introductory blog post or video.

Both of these packages can be installed from npm:

npm install --save immer unionize

First let’s see how to apply Immer and Unionize to a very basic example. I’ll use the example of a counter from the NgRx documentation. I’ve updated the code to use Immer and Unionize, you can find it in this GitHub repo or launch it on Stackblitz to see it working.

This is what the actions look like in the NgRx example:

Now they look like this:

Unionize does all the hard work, defining everything we need to have strongly typed actions. Because Unionize is a more general TypeScript library, we need to customise it a bit to work with NgRx. The option { tag: 'type'} passed in the example above customises the actions to use type as the key. This is what NgRx expects as the key that defines they type of action.

This first example of Unionize is very basic. It gets more interesting with more complex actions that include payloads.

In the original NgRx example, the reducer used to look like this:

Now it looks like this:

The Actions.match function is created automatically by Unionize and can be used to easily create a reducer. You just provide a map of functions where the keys of the map relate to the appropriate action tags (type). To work with NgRx you also always need to provide the default case, as all actions get passed to all reducers, and you might have multiple reducers in different feature modules. In this case the default option is a noop () => {} .

I have combined the match functionality of Unionize with the immutable features of Immer. Usually the functions in the match map would have to return a new state and not mutate the previous state. But, thanks to Immer we can operate on a draft copy of the new state. This can be mutated using standard JavaScript techniques. Immer will take care of generating the new immutable version of the state for us.

You can see there’s a lot less boilerplate, but it’s not really clear in this basic example that there are many other benefits. Immer is lightweight, performant, and simple. It gives us the performance advantages of structural sharing that are even more evident when using the ChangeDetection.OnPush strategy. And, unlike Immutable.js, it interoperates seemlessly with standard JavaScript.

Not yet convinced? How about the obligatory Todo app example.

You can view this in this GitHub repo, or launch it directly on StackBlitz.

NB: In order to use AOT (i.e. production builds) the exported reducer function must be declared as a function, not a const. Use something like this:

export function reducer(state = initialState, action) {
  return produce(producer)(state, action);
}

or, if you prefer…

export const reducerProducer = produce(producer, initialState);

export function reducer(state, action) {
  return reducerProducer(state, action);
}

For more info check the Immer and Unionize repos. Drop me a line if this was useful, or if you have any questions.