Alex Sidorenko

Why is it so difficult to modify a deeply nested state in React?

June 28, 2021

It’s not uncommon to see a code like that in React world. All this effort just to update a single to-do item inside a nested state.

const updateTodo = ({ taskId, todoId, value }) => {
  setProject({
    tasks: {
      ...state.tasks,
      [taskId]: {
        ...state.tasks[taskId],
        todos: {
          ...state.tasks[taskId].todos,
          [todoId]: {
            value: value,
          },
        },
      },
    },
  })
}

But how come that such a popular, widely spread tool as React makes such a hustle from something that a beginner may bump into during the first project? Is that really so that solo React can not tackle deeply nested state updates with less boilerplate? Isn't it a bit disappointing?

Immutability trade-off

React embraces the concept of immutability. The above code example updates the state immutably. It means that it doesn’t modify the state directly. Instead, it creates a new copy of the state. In comparison, this how a direct mutation of the state would look like:

project.tasks[taskId].todos[todoId].value = true

But why is immutability so important if it makes the trivial task of updating a state so complicated? Here are three main reasons from React documentation:

I

Immutability makes complex features much easier to implement. Avoiding direct data mutation lets us keep previous versions of the state history intact, and reuse them later.

II

Detecting changes in mutable objects is difficult because they are modified directly. This detection requires the mutable object to be compared to previous copies of itself and the entire object tree to be traversed.

III

The main benefit of immutability is that it helps you build pure components in React. Immutable data can easily determine if changes have been made, which helps to determine when a component requires re-rendering.

More information on immutability:

Ok, I get it - immutability is important, but how do I update my nested state?

There are two main ways to simplify nested state updates in React - flattening the state and using immutable libraries.

Flattening the state

The best solution is to avoid the problem. If you have a nested state, try to flatten it. Check out three main reasons to keep your state flat by the Redux maintainer, Mark Erikson. You can try to flatten your state by hand or use a third-party library, like Normalirz.

Using immutable libraries

There are libraries out there that are designed to help with immutable updates. For example, here is how Immer can help to reduce our boilerplate.

const updateTodo = ({ taskId, todoId, value }) => {
  setState(
    produce(baseState, draftState => {
      draftState.tasks[taskId].todos[todoId].value = value
      return draftState
    })
  )
}

Check out top 13 immutable libraries

Summary

The pain of updating a nested state stems from the fundamental architectural decision of the React to embrace immutability. Immutability comes with plenty of great benefits, such as predictability and performance.

There are two main ways to deal with the problem of updating a deeply nested state. First is flattening your state to avoid the problem altogether. The second is using immutable libraries that help with state manipulations.