Immer — Javascript Immutability the happy way

Huy Trinh
7 min readJan 10, 2020
Photo by Kobby Mendez on Unsplash

Prerequisite: Basic knowledge about React and Immutability in Javascript

In this post, I am going to talk about Immer, a library that makes immutability in Javascript way easier and simpler.

I assume that you already know why we need immutability. If you don’t, no worries, check this blog first 😍

🍹 Spoiler alert

If you want to make a mixed drink, pour wine and sodas into a glass, not the sodas into the bottle of wine. We call it Immutable Bottle of Wine

I let you decide why we should do that

💪 Let’s get started!

1. Immutability in Javascript

Back to the first time I learned React, I only know one way to make the state immutable and I bet you are familiar with it too

Yes, you are absolutely right. Let’s talk about …

⭐ Spread operator

Our task today is to make a mixed drink for the New Year.

Discount 50% for anyone reading this blog

Our Happy Menu

🍷 The infamous mutable bottle of wine

One day, our new bartender got drunk, so he poured the sodas into the bottle of wine. Therefore, that bottle of wine was spoiled badly ⚠️

Next day, he used that wine bottle to mix other drinks to serve the guests. Of course, other drinkers did not realize that it is no longer the original drink but they could spot after tasting it 🐛🐛

const bottleOfWine = ['wine']

function mixWineAndSoda(bottleOfWine) {

bottleOfWine.push('soda') // Opps, he spoiled the bottle of wine with sodas
}

mixWineAndSoda(bottleOfWine)

console.log(bottleOfWine) // ['wine', 'soda']

We modified the bottleOfWine array by accident when we put it into the mixWineAndSoda function. Imagine that we use this bottleOfWine in many functions and keep modifying it. It's really hard to debug and keep track of which function adding what to the bottleOfWine and what if we want to use our original array 🙃

🍹 The famous immutable bottle of wine

This drink is only for experienced coders who want to learn the correct way to mix wine and sodas

const bottleOfWine = ['wine']

function mixWineAndSoda(bottleOfWine) {

// pour wine from bottle into a glass
const wineGlass = {...bottleOfWine}

// add soda
wineGlass.push('soda')

return wineGlass
}

const mixedDrink = mixWineAndSoda(bottleOfWine)

console.log(bottleOfWine) // ['wine']
console.log(mixedDrink) // ['wine', 'soda']

By making a copy of bottleOfWine then modify it, we prevent ourselves from imutating our original array

🤫 Spread operator is really cool. However, it could be painful when it comes to really nested object

Let’s do a small task: Change the address of our bar from Paris to New York without mutate the barInfo object

const barInfo = {
address: {
country: {
city: 'Paris'
}
}
}

🤫 Honestly, I struggled to do this task. Thanks to Netflix and The Witcher for helping me

🎶 Toss a coin to your Witcher. A friend of humanity

const updatedBarInfo = {
...barInfo,
address: {
...barInfo.address,
country: {
...barInfo.address.city,
city: 'New York'
}
}
}

console.log(barInfo.address.country.city) // Paris
console.log(updatedBarInfo.address.country.city) // New York

⭐ ImmutableJS

There are other ways to achieve immutability including Object.assign or ImmutableJS. However, I find it complicated to use ImmutableJS as we have to learn and understand the whole new API to use it.

Let’s take a quick look 🙄

import {fromJS} from 'immutable'

const barInfo = fromJS({
address: {
country: {
city: 'Paris',
},
},
})

const updatedBarInfo = barInfo.updateIn (
['address', 'country', 'city'],
value => 'New York',
)

console.log(barInfo) //Map {size: 1, _root: ArrayMapNode, ...}
console.log(barInfo.toJS().address.country.city) // Paris

console.log(updatedBarInfo) //Map {size: 1, _root: ArrayMapNode, ...}
console.log(updatedBarInfo.toJS().address.country.city) // New York

As you can see, we have to wrap the barInfo object within fromJs function to make it immutable. We then use updateIn to modify the city value. Note that barInfo is no longer a normal Javascript object, it becomes Immutable.Map. To turn it back to normal Javascript object, we have to use toJS().

And that’s just a small part of ImmutableJS API

We have to learn the whole new API to use ImmutableJS effectively 👽

2. Immer in Javascript

All you need to remember is that Immer has a produce function that allows us to create a draft. By modifying the draft, we avoid mutating the original object.

produce(currentState, producer: (draftState) => void): nextState

💪 Let’s take a look at our example

First, we wrap our object or array within the produce function then we can modify the draft without the fear of mutating the original object/array.

import produce from 'immer'

const bottleOfWine = ['wine']

function mixWineAndSoda(bottleOfWine) {

const wineGlass = produce(bottleOfWine, draft => { // draft is our glass
draft.push('soda') // add soda
})

return wineGlass
}

const mixedDrink = mixWineAndSoda(bottleOfWine)

console.log(bottleOfWine) // ['wine']
console.log(mixedDrink) // ['wine', 'soda']

Immer shows its magic when it comes to nested object since we can modify the draft as the way we do with normal javascript object or array

import produce from 'immer'

const barInfo = {
address: {
country: {
city: 'Paris'
}
}
}

const updatedBarInfo = produce(barInfo, draft => {
draft.address.country.city = 'New York' 🔥
})

console.log(barInfo.address.country.city) // Paris
console.log(updatedBarInfo.address.country.city) // New York

📢 Happy new year to everyone

3. Immer in React:

In React applications, we normally want to make sure our state is immutable.

Let’s see how Immer works in React application

🔥 Immer with Producer in Redux State

In this example of Redux State, we want to update the value of label from Cocktail to Martini without mutating our original state. We can achieve that using Spread operator

const initialState = {
data: {label: 'Cocktail'},
isLoading: false
}

const reducer = (state = initialState, action) => {
switch(action.type) {
case CHANGE_LABEL:
return {
...state,
data {
...state.data,
label: 'Martini'
}
}
}
}

💪 Let’s use Immer to simplify our reducer

produce(currentState, producer: (draftState) => void): nextState

import produce from 'immer'

const initialState = {
data: {label: 'Cocktail'},
isLoading: false
}

const reducer = (state = initialState, action) => {
return produce(state, draft => {
switch(action.type) {
case CHANGE_LABEL:
draft.data.label = 'Martini'
break
}
})
}

We use produce function to wrap our original state and then modify the draft. The produce function automatically returns a new state for us if we updated the draft.

🔥 Immer with Curried Producer in Redux State

We can even make it simpler by using Curried Producer 💪

If you work with functional programming, you will be familiar with the Currying concept. I will not cover the functional programming concepts here and if you don’t work with functional programming, you can just accept the Curried Producer as a new syntax.

⚠️ With Curried Producer, the state is omitted and the initialState is passed as a second argument of produce

💪 Normal Producer

import produce from 'immer'

const reducer = (state = initialState, action) => {
return produce(state, draft => {
switch(action.type) {
case CHANGE_LABEL:
draft.data.label = 'Martini'
break
}
})
}

💪 Curried Producer

import produce from 'immer'

const reducer = produce(draft, action) => {
switch(action.type) {
case CHANGE_LABEL:
draft.data.label = 'Martini'
break
},
initialState
}

You may ask what if you want to get the original state within the produce since the state is omitted. original comes into rescue 😎

import produce, {original} from 'immer'

const reducer = produce(draft, action) => {
switch(action.type) {
case CHANGE_LABEL:
original(draft.data) // In case you really want to get data from the original state
draft.data.label = 'Martini'
break
},
initialState
}

🔥 Immer in Component State

I will go through really quick without much explanation since it is the same as we’ve discussed above. However, I want to introduce you the use-immer library

In our example, we use React.useState hook for state management and we can update the state via updateBottleOfWine function

💪 Normal producer

import React from 'react
import produce from 'immer'

const App = () => {
const [bottleOfWine, setBottleOfWine] = React.useState(['wine'])

function updateBottleOfWine() {
setBottleOfWine(state => produce(state, draft => {
draft.push('sodas')
})
}
}

💪 Simplify with Curried Producer

Pay attention to updateBottleOfWine function to see how we omit the state

import React from 'react
import produce from 'immer'

const App = () => {
const [bottleOfWine, setBottleOfWine] = React.useState(['wine'])

function updateBottleOfWine() {
setBottleOfWine(produce(draft => { // 👈
draft.push('sodas')
})
}
}

💪 Simplify with use-immer

We use useImmer instead of React.useState then we can just update the state directly without worrying about mutating the original state.

import React from 'react
import {useImmer} from 'use-immer'

const App = () => {
const [bottleOfWine, setBottleOfWine] = useImmer(['wine']) // 👈

function updateBottleOfWine() {
setBottleOfWine(draft => {
draft.push('sodas')
})
}
}

4. Conclusion:

Immer is a Javascript library that makes immutability way simple. By using Immer, we can find it easy to modify nested objects without the fear of mutating it. It is very straightforward to use Immer as we can modify object or array as the way we used to, without having to adopt the whole new API. 👏👏👏

Here are some good resources for you:

🙏 💪 Thanks for reading!

I would love to hear your ideas and feedback. Feel free to comment below!

✍️ Written by

Huy Trinh 🔥 🎩 ♥️ ♠️ ♦️ ♣️ 🤓

Software developer | Magic lover

Say Hello 👋 on

Github

LinkedIn

Medium

--

--