Improving complex state updates using use-immer library

Improving complex state updates using use-immer library

In React, component rerender is the expected behavior when there is a change to the stored state within / passed to it. This gets interesting when dealing with states ( objects ) where the concept of ‘change’ is not as straightforward as it might seem to be.

In this article, I will brush over the concept of changing( updating ) state in react using the useState hook , give a scenario where using useState can get complicated and how this can be simplified using the useImmer hook from use-immer package .

Updating simple state objects in react.

An update to state objects requires that the current state is replaced by an entirely new object as opposed to directly modifying the object in state. Direct modification of state object in a react component won’t cause the normal behavior expected - component rerender. Consequently, the dom won’t be updated and this can be a source of bugs which could have been avoided easily.

Let’s take a Simple Box component as an example.

In this component, there is a button that updates( increments ) the left property of the position state object when it is clicked on and a paragraph tag to display the value of this left property.

import { useState } from "react";

function Box() {
  const [position, setPosition] = useState({
    left: 0,
    right: 0,
    top: 0,
    bottom: 0,
  });

  // ❌❌❌❌ - This doesn't give expected behavior
  // const updatePosition = () => {
  //  position.left = position.left + 1;
  // };

  // ✅✅✅✅ - This gives expected behavior
  const updatePosition = () => {
       setPosition({...position,left: position.left + 1})
   }

  return (
    <>
      <h1>Simple Box</h1>
      <p>left: {position.left}</p>
      <button onClick={updatePosition}> Update Left Position </button>
    </>
  );
}

export default Box;

💡 The spread operator (...) enables us to create an entirely new object to which we can make our change - increment value position's left property.

Updating the position state object in the above code is pretty much simple.

Updating complex states.

Let’s take another example ( I’ll call it “Complex Box Example” )where the state object is a complex/nested object.

import { useState } from "react";

function ComplexBox() {
  const [info, setInfo] = useState({
    name: "BoxA",
    properties: {
      size: "medium",
      color: "black",
      position: {
        left: 0,
        right: 0,
        top: 0,
        bottom: 0,
      },
    },
  });

  const updatePosition = () => {
    setInfo({
      ...info,
      properties: {
        ...info.properties,
        position: {
          ...info.properties.position,
          left: info.properties.position.left + 1,
        },
      },
    });
  };

  return (
    <>
      <h1>Complex Box</h1>
      <p>left: {info.properties.position.left}</p>
      <button onClick={updatePosition}> Update Left Position </button>
    </>
  );
}

export default ComplexBox;

🧐 Seen an issue ?

The example above produces the same result as the very first one but a bit of complexity was involved. Unlike the Simple Box example, the Complex Box example required spreading -the effect of needing to modify inner properties of complex object states - the previous state object( and its properties ) before an update could be made.

Apparently, the length of code written for the ‘Complex Box’ is more in comparison with that for the ‘Simple Box’. This can hamper code maintainability and readability and be an easy gateway for bugs in our code.

Just imagine, for some reason, the state object has to be more complex relative to that in the Complex Box example.😶

An attempt to update the state directly ( as shown below ) without having to “spread” at each level of nested object can be tempting.

import { useState } from "react";

function ComplexBox() {

   // ...
   const updatePosition = () => {
          info.properties.position.left = info.properties.position.left + 1
   }

   return (...);
}

export default ComplexBox;

But doing this will imply mutating the state directly which is one of the things React doesn’t recommend. In addition, the useState hook assumes any state that is stored inside it is treated as immutable.

This doesn’t mean process of updating the state must be completely void of mutation. ( Just ensure that in no way , you are not modifying the state ).

Now, it’s time to improve the state update of the ComplexBox component for better readability and maintainability.

Improving complex states update.

The use-immer package provides hooks useImmer and useImmerReducer that are very similar to useState and useReducer react hooks. These use-immer hooks simplify deep updates within complex states.

You can install the package by running the following command in your terminal.

npm i use-immer

State update in our Complex Box example can now be simplified.


import React from "react";
import { useImmer } from "use-immer";

export default function ComplexBoxWithImmer() {
  const [info, setInfo] = useImmer({
    name: "BoxA",
    properties: {
      size: "medium",
      color: "black",
      position: {
        left: 0,
        right: 0,
        top: 0,
        bottom: 0,
      },
    },
  });

  const updatePosition = () => {
    setInfo(
        (draft) => {
          // Note : `draft.properties.position.left += 1` is equivalent to `draft.properties.position.left = draft.properties.position.left + 1`
        draft.properties.position.left += 1;
      }
    );
  };

  return (
    <>
      <h1>Complex Box With Immer</h1>
      <p>left: {info.properties.position.left}</p>
      <button onClick={updatePosition}> Update Left Position </button>
    </>
  );
}

And yea 🤗 , there is a glaring difference.

Now, there are no spread operators sprinkled here and there in our code and the state update follows the same idea as modifying objects in javascript. Updating the state is much more simpler and consequently, our code is relatively more readable, maintainable, and less prone to bugs.

useImmer hook returns a tuple ( [ state, stateUpdater]) just like useState. stateUpdater takes an updater function that’s passed a draft state( a complete clone of the state ) which can be modified within it’s body.

Wrap Up.

The use-immer package shouldn't be used unnecessarily as it functions exactly as useState hook. Personally, I find it necessary when dealing with complex / deeply nested states that require deep updates. Also, the use-immer hooks can handle arrays and not just objects only.

Like useImmer and useState, there is no difference in the usage of useImmerReducer in place of useReducer. An example of this usage can be found in one of the links below.

Feel free to reach out to me on Linkedin and Twitter.

use-immer doc: https://immerjs.github.io/immer/example-setstate

useImmerReducer usage example: https://newbedev.com/javascript-react-useimmer-code-example

What's Immutability? : https://www.dottedsquirrel.com/immutability-javascript/