Achieving immutability using Immer

Namrata Krishnawat
mirafra-sw-engineering
4 min readSep 7, 2020

Immer is a light-weight javascript package used for working with immutable state in a very convenient way. Use of immer simplifies deep updates in the state of app components.

For any medium-sized-application, there will always be a state (in fact, a lot of state). On top of that, there will be asynchronous actions that will keep updating the state. Predicting the state and tracking mutations which cause side-effects becomes a tedious job. So keeping the state immutable can help us in terms of performance, predictability and better mutation tracking. We can always predict the state at any given time.

While the methods of using Object.assign() or spread operator are used for data mutation by creating new object copies and then mutating their values, the process becomes really complex when nested data (especially for arrays and objects) comes into picture (which can be easily seen in large projects).

A few libraries like Immutable.js achieve the same, but it requires us to study a whole new API to be able to use them.

This is where Immer comes to rescue as it achieves immutability in a much more streamlined manner. It removes all the overhead of verbose syntax and leads to clean and concise code. The code for using immer is inline with the standard javascript so it is easy to understand.

Usage:

To add immer:

yarn add immer

A simple example of immer usage is::

import produce from “immer”;const nextState = produce(baseState, draftState => {
draftState.name = “John”
draftState.age = 22
})

Here, the baseState is the current state of our app. A temporary copy of the current state is created which is the draftState. DraftState acts as a proxy of current state. All new changes are applied to this proxy state first and once all data mutation is done, we get our nextState. Thus enabling immutability of state in a much easier way.

How Immer works behind the scene:

Immer creates a temporary shadow tree which can be modified using the standard JavaScript APIs. The shadow tree is used to generate the next immutable state tree. This shadow tree is maintained using Proxies. Proxies are an exotic javascript feature that is nowadays available in all modern browsers.

const proxy = new Proxy(target, traps)

The target is the original object which you want to proxy. The traps are objects that define which operations will be intercepted and how to redefine intercepted operations.

Initially when the producer starts, a proxy is created, which is the draft object we pass to our function. Now as we traverse through the tree and try to modify something, it will immediately create a shallow clone of the target node and mark it as “modified”. This process is called “copy-on-write”. After all modifications are done we get a proxy tree which acts as a shadow of our base tree. Upon completion, the producer combines the clones and freezes the modified objects. This is how immutability is achieved using Immer.

Immer can be used wherever deep data mutation is required . For example, setState() function in React or Reducers in Redux.

this.setState(prevState => 
({ items: {
...prevState.items,
count: prevState.items.count + 1 }
})
)

But by using produce from immer, it simple becomes :

this.setState(
produce(draft => {
draft.items.count += 1
})
)

Let us try to understand immer in reducers with example.

Reducer using Object.assign or spread operator

Let us see an example of how immer makes the code simpler.

This is the traditional method of mutating data in our reducers.

Reducer using Immer — produce

As shown below, the Object.assign() and the spread operators are replaced by produce and the code becomes easier to understand.

Advanced usage using curried Producer

Using curried Producer further reduces the code . It takes a function as first argument which means we get a pre-bound producer that only needs a state to produce the value from. Initial state can be provided as second argument here.

Usage of useImmer hook

We also have a hook called “useImmer” if we want to use immer with react-hooks

yarn add use-immer

The sample usage is as follows:


import { useImmer } from ‘use-immer’;
const [todo, setTodos] = useImmer([]);const onAddTodo = (newTodo) => {
setTodos(draft => {
draft.push(newTodo);
});
}

Conclusion

Immer makes enforcing immutability very smooth. It makes life easier when mutating nested data structures. And as we saw, the APIs for immer are very simple, easy to use, easy to learn and have a clean and efficient syntax. The interoperability with standard JavaScript and performance improvements just add icing on the top.

If you want to check out the entire code, please visit https://github.com/Nam92rata/Todo-app-with-Immer

Thanks for reading !

--

--