Update complex state using immutability-helper or immer

In React apps, state should be immutable. For every state change in our component, we should create a new state object. Consider the following state object.

const [complexState, setComplexState] = useState({
    data: [
      {
        id: 1,
        message: "Hello",
        read: false
      },
      {
        id: 2,
        message: "World",
        read: false
      }
    ]
  });

State object has a data property. This is an array of objects. Each object has an id, message and read. Let’s say I have the following component to toggle read status.

    <div className="App">
      {complexState.data.map(cs => (
        <div key={cs.id}>
          {cs.message}
          <input
            type="checkbox"
            checked={cs.read}
            onChange={handleChange.bind(null, cs.id)}
          />
        </div>
      ))}
    </div>

Update state using plain JS

How should the handleChange handler look like? Create a new state object, Create a new data array. Pick the item to update. Create a new object at the array index. Toggle the read flag. It translates into the following code.

function handleChange(id) {
    const data = complexState.data.slice();
    const index = data.findIndex(cs => cs.id === id);
    if (index > -1) {
      data[index] = {
        ...data[index],
        read: !data[index].read
      };
      setComplexState({
        ...complexState,
        data
      });
    }
  }

Though code is logical, it is a bit messy. This is where some sort of immutability helper helps.

Update state using immutability-helper

React website recommends using the package immutability-helper. This package makes state updates easier. With this package, the code for doing the previous state update looks like the below.

function handleChangeWithHelper(id) {
    const index = complexState.data.findIndex(cs => cs.id === id);
    if (index > -1) {
      const newComplexState = update(complexState, {
        data: {
          [index]: {
            $apply: function(item) {
              return {
                ...item,
                read: !item.read
              };
            }
          }
        }
      });
      setComplexState(newComplexState);
    }
  }

There is a helper function update from immutability-helper which eases the code involved in state updates. The function takes the original state object and another object which specify the state changes. The second parameter has the same hierarchy as the original state object. In our example, we use the index to modify just one item in the array. That one item is passed to an $apply function which toggles the read status.

This code is a bit of an improvement with the plain JS code. There is another package immer which I prefer to do state changes.

Update state using immer

Immer has a produce function that takes the original state object and a update function which acts upon a draft state object. The following bit of code is self-explanatory.

  function handleChangeWithImmer(id) {
    const newComplexState = produce(complexState, draft => {
      const index = draft.data.findIndex(d => d.id === id);
      if (index > -1) {
        draft.data[index].read = !draft.data[index].read;
      }
    });
    setComplexState(newComplexState);
  }

Immer does the heavy-lifting of creating new objects and arrays. And we just operate on the draft state as if it is a new object that is deep copied from the original object.

All the source code used in this article is available in CodeSandbox.

Related Posts

Leave a Reply

Your email address will not be published.