Immutable, structurally shared data structures are a great paradigm for storing state. Especially when combined with an event-sourcing architecture. However, there is a cost to pay. In a language like JavaScript where immutability is not built into the language, producing a new state from the previous one is a boring, boiler-platy task. To prove the point: The Redux-ecosystem-links page alone lists 67(!) packages to help you to deal with immutable data structures in Redux.

And still; most of them don’t solve the root problem: lack of language support. For example, where update-in is an elegant concept in a language like ClojureScript, any JavaScript counterparts will basically rely on ugly string paths. Which are error-prone, hard to type-check and requires learning yet another set of API functions by heart to be pro-efficient.

So what if we stopped fighting the language and embraced it instead? Without giving up on the elegance provided by persistent data structures. That is exactly what immer does.

Tip: if you don’t like reading, you can also watch the egghead tutorial for immer

Producers

Immer works by writing producers, and the simplest producer possible looks like this:

A minimal (empty) producer will return the original state

The produce function takes two arguments. The currentState and a producer function. The current state determines our starting point, and the producer expresses what needs to happen to it. The producer function receives one argument, the draft, which is a proxy to the current state you passed in. Any modification you make to the draft will be recorded and used to produce nextState. The currentState will be untouched during this process.

Because immer uses structural sharing, and our example producer above didn’t modify anything, the next state above is simply the state we started with.

Let’s take look at what happens when we start modifying the draft in our producer. Note that the producer function doesn’t return anything, the only thing that matters are the changes we make.

A real producer. All changes to draft are reflected in the next state, which structurally shares untouched items with the previous state

Here we actually see produce in action. We created a new state tree, which contains one extra todo item. Also, the status of the second todo was changed. These where the changes we applied to the draft, and they are nicely reflected in the resulting next state.

But there is more. The last statements in the listing show nicely that the parts of the state that were modified in the draft have resulted in new objects. However, unchanged parts are structurally shared with the previous state. The first todo in this case.

A reducer with a producer

Now we learned the basics of producing a new state. Let’s leverage this in an exemplary Redux reducer. The next gist is based on the official shopping cart example, and loads a bunch of (possibly) new products in the state. The products are received as an array, transformed using reduce, and then stored in a map with their id as key.

A typical Redux reducer

The boilerplaty part here is:

We have to construct a new state object, in which the base state is preserved and the new products map is mixed in. It is not too bad in this simple case, but this process has to be repeated for every action, and on every level in which we want modify something. We have to make sure to return the existing state if the reducer doesn’t do anything

With Immer, we only need to reason about the changes we want to make relatively to the current state. Without needing to take the effort to actually produce the next state. So, when we use produce in the reducer, our code simply becomes:

Simplifying the reducer by using Immer

Notice how much easier it is to grasp what RECEIVE_PRODUCTS is actually doing? The noise has largely been removed. Also note that we don’t handle the default case. Not changing the draft simply equals returning the base state. Both the original reducer and the new one behave exactly the same.

No strings attached

The idea to produce the next immutable state by modifying a temporarily draft isn’t new. For example immutableJS provides a similar mechanism: withMutations. The big advantage of Immer however, is that you don’t have to learn (nor load) an entire new library for your data structures. Immer operates on normal JavaScript objects and arrays.

The advantages go even further. To reduce boilerplate, ImmutableJS and many others allow you to express deep updates (and many other operations) with dedicated methods. These paths however are raw strings and cannot be verified by type-checkers. They are pretty error prone. In the following listing for example the type of list cannot be inferred in the ImmutableJS case. Other libraries take this even a step further and even fiddle their own DSLs into these path queries, enabling more complex commands like splices. At the cost of introducing a mini-language in the language.

Immer remains typed doing deep updates

Immer doesn’t suffer from any of that; it operates on built-in JavaScript structures. Perfectly understood by any type-checker. And modifying data is done through APIs you are already familiar with; the ones built into the language.

Auto freeze

Another cool(😒) feature of Immer is that it will automatically freeze any data structure you created using produce . (In development mode). So that you get truly immutable data. Where freezing the entire state would be pretty expensive, the fact that Immer can just freeze the changed parts makes it pretty efficient. And, if all your state is produced by produce functions, the effective result will be that your entire state is always frozen. Which means you will get an exception when you try to modify the state in any way.

Currying

Ok. One last feature: So far we have always called produce with two arguments, the baseState and a producer function. However, in some cases, it can be convenient to use partial application. It is possible to call produce with just the producer function. This will create a new function that will execute the producer when it’s passed in a state. This new function also accepts an arbitrary amount of additional arguments and passes them on to the producer.

Don’t worry if you couldn’t parse the last sentences. What it boils down to is that you can further reduce the boilerplate of reducers by leveraging currying: