Meet Immer.js — the immutability helper

Scalac
6 min readJun 30, 2022

--

immutability helper

In recent years the Front-End world has been shaken by the emergence of a lot of libraries and frameworks based on web components, which have flooded pages that until then had been based on jQuery and Backbone. Between those, and the arrival of React and its states, doors opened up to a brand new reactive world of immutability. React’s reactivity demands immutable states, which force us to deep clone it first whenever we want to change a state that is object-based. Then, we also need to obtain a new memory reference to it, thus helping React’s internal reactivity mechanism to detect the changes faster and do its job with less effort. Therefore, whenever we want to change a state object, we need to clone it first, something that would normally be very cumbersome. However, with the advent of the Spread Operator, this has now become an easy task, because along with other various points, it is a great help when it comes to cloning objects. Every time we need to change/clone a deep object it will be there; the three dots, increasingly appearing in more and more React codes. Here we have an example of how to change an object using the spread operator, cloning the oldObject and changing only the parts that are important to us to change:

const oldObject = {
section: {
name: 'Admnistrative',
manager: {
name: 'Lucas',
address: {
street: 'Mario Benedicto Costa',
number: 20
}
}
}
}
const newObject = {
...oldObject,
section: {
...oldObject.section,
manager: {
...oldObject.section.manager,
address: {
...oldObject.section.manager.address,
number: 21
}
}
}
}

However, even though it is an excellent tool, there are still some problems in using the spread operator for cloning objects. Firstly it is very verbose, secondly, it is manual. In other words, you will need to remember to use it, because you won’t receive an error if you forget, and being human, we are always apt to forget details like this.

But when working with Immutability in Javascript and in React, there is another problem: Freeze Objects. In Javascript, we have a function called freeze, which helps us to freeze objects but isn’t compatible with everything in Javascript. We often have problems with it, mainly because it only shallow freezes objects, and so is not a good option for nested objects for example. This is a recurring problem in the Javascript world: when we want to use a resource that is not available in the language natively, we need to wait for it to be discussed, approved and implemented through an RFC. Or we need to use some superset of Javascript or a library to help us make use of the resources. It is no different with Functional Programming resources. When we want to enjoy the powers of Javascript, we need to use a library, such as Ramda.js, Immutability.js or Immer.js, the latter being the principal subject of this article. Immer.js — the Immutability helper is a library that allows us to use Immutability in Javascript, without the need to learn a new paradigm or change the way we do our code. It provides us with the possibility to handle Immutability using plain Javascript objects, without the need to use the spread operator. It is less verbose and so much faster than other similar libraries. It also helps by being a kind of sandbox for our code, throwing errors when we try to change any Immutable Object, acting as a layer on top of our objects, due to the fact it is proxy-based. Immer.js gives us a lot, from just 3kb (gzipped), using only producer proxy functions. The same code as above would become:

import produce from 'immer'const oldObject = {
section: {
name: 'Admnistrative',
manager: {
name: 'Lucas',
address: {
street: 'Mario Benedicto Costa',
number: 20
}
}
}
}
const newObject = produce(oldObject, draft => {
draft.section.manager.address.number = 21;
})

Note that we only need to change what is important to us, without the need to clone any other parts of the object. On top of that, Immer.js also has a great integration with Typescript, being smart enough to understand when you want to change a protected/read-only property when creating a new copy of an object (it allows you to change only while it is a draft object, after which it continues to be a read-only object). It also respects typing and is intellisense integrated. But can this immutability helper actually help us when using it alongside React? The answer is, yes. It can help us when handling changes in states. For example, when using the React Class-Based API, we could use it like this:

import React from 'React'
import produce from 'immer'
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
nameInputValue: '',
telephoneInputValue: '',
user: {
name: 'Lucas',
contacts: []
}
}
}
handleContactNameInputChange = e => {
this.setState(
produce(draft => {
draft.nameInputValue = e.target.value
})
)
}
const handleContactTelephoneInputChange = e => {
this.setState(
produce(draft => {
draft.telephoneInputValue = e.target.value
})
)
}
addContact = () => {
const newContact = {
name: this.state.nameInputValue,
telephone: this.state.telephoneInputValue
}
this.setState(
produce(draft => {
draft.user.contacts.push(newContact)
})
)
}
}

The only thing now needed is to add the produce function inside the setState and it will do its job. Class api is an api increasingly being replaced by the Hooks api, and is already being used by a lot of companies, so it is nice that Immer.js is also compatible with that. Similarly, with the Hooks api, we need to use the produce function inside the functions returned by the useState hook, once they already expect an immutable answer:

import React, { useState } from 'React'
import produce from 'immer'
const UserContacts = () => {
const [nameInputValue, setNameInputValue] = useState('')
const [telephoneInputValue, setTelephoneInputValue] = useState('')
const [user, setUser] = useState({
name: 'Lucas',
contacts: []
})
const handleContactNameInputChange = e => {
setNameInputValue(e.target.value)
}
const handleContactTelephoneInputChange = e => {
setTelephoneInputValue(e.target.value)
}
const addContact = () => {
const newContact = {
name: nameInputValue,
telephone: telephoneInputValue
}
setUser(
produce(
draft => {
draft.user.contacts.push(newContact)
}
)
)
}
}

The same happens when using Typescript, we only need to remember to use the produce function:

import React, { useState } from 'React'
import produce from 'immer'
type Contact = {
name: string,
telephone: string
}
type User = {
name: string,
contacts: Contact[]
}
const UserContacts = () => {
const [nameInputValue, setNameInputValue] = useState('')
const [telephoneInputValue, setTelephoneInputValue] = useState('')
const [user, setUser] = useState<User>({
name: 'Lucas',
contacts: []
})
const handleContactNameInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNameInputValue(e.target.value)
}
const handleContactTelephoneInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setTelephoneInputValue(e.target.value)
}
const addContact = () => {
const newContact: Contact = {
name: nameInputValue,
telephone: telephoneInputValue
}
setUser(
produce(
draft => {
draft.user.contacts.push(newContact)
}
)
)
}
}

The benefits of using Immer.js become more apparent when we use a library for state-sharing such as Redux. In this case, whenever we create our Reducers, we can save a lot of code using Immer.js, because we always need to clone our state and change something in it, according to the current action. This is one of the places where Immer.js — the immutability helper shows its brilliance. Let’s see an example of a simple reducer, without using Immer.js, and the same version using Immer.js:

import React, { useState } from 'react';
import produce from 'immer';
const INITIAL_STATE = {
user: {
name: 'Lucas'
}
}
export default function contactsReducer(state = INITIAL_STATE, action) {
switch (action.type) {
case 'contacts/contactAdded': {
return {
...state,
user:
{
...user,
contacts: [
...user.contacts,
{
...action.payload
}
]
}
}
}
default:
return state
}
}

And now, the version using Immer.js to handle Immutability:

import React, { useState } from 'react';
import produce from 'immer';
const contactsReducer = produce((draft, action) => {
switch (action.type) {
case "contacts/contactAdded":
draft.user.contacts = {
...action.payload
}
break
default:
break
}
})
export default contactsReducer;

The most impressive thing about the Immer.js library is how much it does with so little, and it does it without changing the developers’ routine as other libraries often do. Another nice thing about it is that it is compatible with the Freeze function from Javascript. When we sometimes have to handle changes in very big objects, it can greatly speed things up, because the freeze function does a shallow freeze and Immer.js can use it as a shortcut to do its job:

import React, { useState } from 'react';
import produce from 'immer';
const myBigObject = {
/* ... */
}
const nextState = produce(freeze(myBigObject), draft => {
/* ... */
})

I hope these few words and examples will motivate you to give it a try. You may find it to be a very powerful tool, and such a great help it could even change your life as a Front-End developer, as it definitely has changed mine. Thanks for reading!

Lucas Caponi da Silva

--

--

Scalac

Scalac is a web & software development company with 122 people including Backend, Frontend, DevOps, Machine Learning, Data Engineers, QA’s and UX/UI designers