immer: Best Way to Update Nested Immutable State

May 29, 2020

Are you looking for a way to update state that's an array or an object that has a nested structure? I have good news. immer makes update nested state incredibly easy.

Let's look at an example first.

The Old Way

In my project imbiased, for the topics I have a state structure like this:

{
    "topicResourceList": [
        {
            "id": 25,
            "userId": 0,
            "userName": null,
            "question": "Coffee or Smoothie",
            "left": {
                "id": 49,
                "name": "Coffee",
                "count": 1
            },
            "right": {
                "id": 50,
                "name": "Smoothie",
                "count": 0
            },
            "commentCount": 4,
            "createdAt": "2020-05-26T01:36:23.0157452",
            "editedAt": "2020-05-26T01:36:23.0157453"
        },
        {
            "id": 24,
            "userId": 0,
            "userName": null,
            "question": "Dog or Cat",
            "left": {
                "id": 47,
                "name": "Dog",
                "count": 1
            },
            "right": {
                "id": 48,
                "name": "Cat",
                "count": 0
            },
            "commentCount": 1,
            "createdAt": "2020-05-26T01:35:04.0794192",
            "editedAt": "2020-05-26T01:35:04.0794192"
        },
    ]
}

Let's say someone created a comment on the first topic. Now I need to update the commentCount in the corresponding topic. The old way of doing it is we have to create a copy of the state and only change the comment count. One way to do it is using map:

return {...baseState, baseState.topicResourceList.map((topic, idx) => {
    if (idx === 0) {
        return {
            ...topic,
            commentCount: topic.commentCount + 1
        }
    }
    return topic
})}

This doesn't seem that bad. What if someone voted? Let's say someone voted for the left side on the first topic.

return {...baseState, baseState.topicResourceList.map((topic, idx) => {
    if (idx === 0) {
        return {
            ...topic,
            left: {
                ...topic.left,
                count: topic.left.count + 1
            }
        }
    }
    return topic
})}

Things are starting to get a little bit messy. If the state is 3, 4 levels deep, updating the state would be a nightmare. Luckily, there's an incredibly easy way to do this: immer.

Using immer

immer makes this tremendously easier, To update commentCount all I have to do in my reducer is:

import produce from "immer"

return produce(baseState, draft => {
    draft.topicResourceList[0].commentCount++
})

To update the vote count:

return produce(baseState, draft => {
    draft.topicResourceList[0].left.count++
})

Isn't that amazing? Along with immer, I discovered redux-toolkit, which gives an opinionated way of using redux and has immer built in.

Normally we put reducers and action creators into separate files, redux toolkit introduces something called the slice, which combines reducers and action creators into one single file. I think using redux-toolkit will make my code cleaner, so in my following projects I may start using redux-toolkit. I definitely recommend checking it out.

Subscribe to my email list

© 2024 ALL RIGHTS RESERVED