Use Immer to Simplify Immutable Data Structures in React

Immutable data structures lead to efficient change detection. This is very important for libraries like React where the Virtual DOM diffing needs to be fast. Immer keeps references in our objects and uses them to tell if an object has changed. Unchanged parts of a data object are kept and shared in memory with older versions of the data object.

In my experience, I’ve found the Immer library to work well when combined with the useReducer hook from React. If you are not familiar with useReducer, it is an alternative to useState, that accepts a reducer of type (state, action) => newState, and returns the current state paired with a dispatch method. This is better than useState especially in situations where you have complex state logic that involves many sub-values, or when next state depends on previous state.

Let’s say we have a situation where we have a list of tasks and want to be able to add new tasks to the list. Also we will like to toggle when a task is complete or not. These various states per task can become daunting when using useState as it is not flexible to new requirements and adding more actions will also pollute the beginning of our functional components. This situation is perfect for the useReducer hook, and combined with Immer, it makes the task less painful to write and read.

produce from Immer gives a reducer function that fits the signature function required for useReducer and we can pass it directly to useReducer. The produce function takes two parameters, draft and action. The draft parameter is a temporary draft, which is a proxy of the currentState. Once all mutations are completed, then Immer will produce a nextState based on the mutations to the draft state. This makes it possible to change our data modifying its state while still getting the benefits of immutable data.

The function passed to produce then behaves like a regular useReducer function with the one caveat that we can mutate the data without worrying about the implications to our application as the data is not truly mutated but instead made immutable in the backend thanks to Immer.

import React, { useReducer, useCallback } from 'react';
import produce from 'immer';
import { uuid } from 'uuidv4';

const Tasks = () => {
  const [tasks, dispatch] = useReducer(
    produce((draft, action) => {
      switch (action.type) {
        case 'add':
          draft.push({
            id: action.id,
            taskName: action.name,
            completed: false,
          });
          break;
        case 'toggle':
          const task = draft.find((task) => task.id === action.id);
          task.completed = !task.completed;
          break;
        default:
          break;
      }
    }),
    [
      /* Initial tasks */
    ]
  );

  const handleAdd = useCallback((name) => {
    dispatch({
      type: 'add',
      id: uuidv4(),
      name,
    });
  });

  const handleToggle = useCallback((id) => {
    dispatch({
      type: 'toggle',
      id,
    });
  });
};

The above code can be made simpler by using useImmerReducer:

import React, { useCallback } from 'react';
import { useImmerReducer } from 'immer';
import { uuid } from 'uuidv4';

const Tasks = () => {
  const [tasks, dispatch] = useImmerReducer(
    (draft, action) => {
      switch (action.type) {
        case 'add':
          draft.push({
            id: action.id,
            taskName: action.name,
            completed: false,
          });
          break;
        case 'toggle':
          const task = draft.find((task) => task.id === action.id);
          task.completed = !task.completed;
          break;
        default:
          break;
      }
    },
    [
      /* Initial tasks */
    ]
  );

  const handleAdd = useCallback((name) => {
    dispatch({
      type: 'add',
      id: uuidv4(),
      name,
    });
  });

  const handleToggle = useCallback((id) => {
    dispatch({
      type: 'toggle',
      id,
    });
  });
};

The combination of Immer and useReducer makes for very concise and legible code that simplifies state in your applications. Reach for this tool next time your state starts to get wild.