DEV Community

TrinhDinhHuy
TrinhDinhHuy

Posted on • Updated on

Immer - Javascript Immutability the happy way

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

immutable

πŸ’ͺ 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']
Enter fullscreen mode Exit fullscreen mode

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']
Enter fullscreen mode Exit fullscreen mode

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

immutable

🀫 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'
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

🀫 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
Enter fullscreen mode Exit fullscreen mode

⭐ 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
Enter fullscreen mode Exit fullscreen mode

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']
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

πŸ“’ 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'
                }
            }
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ’ͺ 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       
        }
    })
}
Enter fullscreen mode Exit fullscreen mode

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       
        }
    })
}
Enter fullscreen mode Exit fullscreen mode

πŸ’ͺ Curried Producer

import produce from 'immer'

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

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
}
Enter fullscreen mode Exit fullscreen mode

πŸ”₯ 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')
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ’ͺ 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')
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ’ͺ 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')
        })
    }
}

Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)