Clean NgRx reducers using Immer
This weeks post is inspired by another great This Dot Media event and the topic this time was state management. There was a small segment about Immer which I found interesting (video is linked at the bottom of this post), so I decided to give it a shot with NgRx.
Like the title says, Immer can reduce the complexity of reducers.
The thing I like most is that it can just be plugged in wherever needed. Introducing Immer doesn’t mean you have to use it in every reducer in your code base.
Immer (German for: always) is a tiny package that allows you to work with immutable state in a more convenient way. It is based on the copy-on-write mechanism.
In this post we’re only using it with a reducer, but it also can be used outside of one.
It is created by Michel Weststrate, the owner of MobX.
Let’s take a look at what a normal NgRx reducer might look like and compare it with the Immer implementation. As an example we’re going to use the classic shopping cart example where we can add and remove items from the cart.
First off, the state, which looks as follows:
A typical NgRx implementation doesn’t mutate the current state and uses the spread syntax to return a new version of the state.
For our cart example, it looks like this:
Now let’s take a look at the Immer way.
The state remains the same and the reducer becomes:
Let’s highlight some points from the snippet above:
produceis a curried function which expects your reducer function as the first argument, and a second argument for the initial state
draftis our current
state, which is typesafe
- changes made to
draftproduces the next state
- We can simply edit our item’s amount
draft.cartItems[action.payload.sku] = …
- We can simply delete products from our cart
And some subtle differences which you may have missed:
- we don’t have to return the
draft, this is because we’re modifying
- there is no default case, the state doesn’t change and will also be the next state
To give another example, let’s take a look at how we load the catalog:
Pretty straight forward, right?
In order to use Immer you’ll have to install it first via
npm install immer, and then import it with
import produce from 'immer’.
If you’re afraid that changing state like this means that every part of your application will be re-rendered, don’t be. Immer uses structurally shared data structures which basically means that only the modified parts of your state will trigger a new result from selectors based of the modified state. Thus, it only re-renders your application where needed.
Immer comes with object freezing out of the box in development. This means that it will throw an error if the state is mutated from outside the
produce function. So if you’re currently not using a library like ngrx-store-freeze and if you’re mutating state outside a reducer, you will get an error thrown at you.
If you still want to mutate your state (which I don’t recommend by the way), you can turn off this feature with
If you build your application in production mode, this check will automatically be skipped for performance reasons.
we’re seeing the redux developer tool in action
Simply put, I like it. But this doesn’t mean I’ll use it everywhere (hint, there is no such thing is a silver bullet), only in places where I see fit. Partly because I like (and I’m used to) writing my reducers in a functional way. But I’ll use it when a reducer becomes too bloated or too hard to reason about.
NOTE: When dealing with collections and CRUD actions you probably (still) want to use @ngrx/entity.
This post is meant to be a short introduction to Immer and to spread the word to the NgRx community. If you like what you’re seeing and want some more details or if you’re interested on how it’s working there are some useful resources below.
- Introducing Immer: Immutability the easy way
- The NgRx repository on GitHub
- The Immer repository on GitHub
Keep up with the advancement of prominent open source frameworks, libraries, and browser standards by attending this online event. Core team members will discuss topics such as upcoming releases, recent milestones, and community initiatives.
Please consider supporting me if have you enjoyed this post and found it useful: