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:
- Why Immutability is so important
- Easy immutable objects in Javascript
- Immutable JavaScript Data Structures with Immer
- ImmutableJS Docs
π πͺ 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)