Back to Immer

React & Immer

website/docs/example-setstate.mdx

11.1.65.2 KB
Original Source
<center> <div data-ea-publisher="immerjs" data-ea-type="image" className="horizontal bordered" ></div> </center> <details> <summary className="egghead-summary"> egghead.io lesson 8: Using Immer with _useState_. Or: _useImmer_ </summary>
<div>
	<iframe
		width="760"
		height="427"
		scrolling="no"
		src="https://egghead.io/lessons/react-immutable-update-state-inside-react-components-with-useimmer/embed"
	></iframe>
</div>
<a
	className="egghead-link"
	href="https://egghead.io/lessons/react-immutable-update-state-inside-react-components-with-useimmer"
>
	Hosted on egghead.io
</a>
</details>

useState + Immer

The useState hook assumes any state that is stored inside it is treated as immutable. Deep updates in the state of React components can be greatly simplified as by using Immer. The following example shows how to use produce in combination with useState, and can be tried on CodeSandbox.

javascript
import React, {useCallback, useState} from "react"
import {produce} from "immer"

const TodoList = () => {
	const [todos, setTodos] = useState([
		{
			id: "React",
			title: "Learn React",
			done: true
		},
		{
			id: "Immer",
			title: "Try Immer",
			done: false
		}
	])

	const handleToggle = useCallback(id => {
		setTodos(
			produce(draft => {
				const todo = draft.find(todo => todo.id === id)
				todo.done = !todo.done
			})
		)
	}, [])

	const handleAdd = useCallback(() => {
		setTodos(
			produce(draft => {
				draft.push({
					id: "todo_" + Math.random(),
					title: "A new todo",
					done: false
				})
			})
		)
	}, [])

	return <div></div>
}

useImmer

Since all state updaters follow the same pattern where the update function is wrapped in produce, it is also possible to simplify the above by leveraging the use-immer package that will wrap updater functions in produce automatically:

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

const TodoList = () => {
  const [todos, setTodos] = useImmer([
    {
      id: "React",
      title: "Learn React",
      done: true
    },
    {
      id: "Immer",
      title: "Try Immer",
      done: false
    }
  ]);

  const handleToggle = useCallback((id) => {
    setTodos((draft) => {
      const todo = draft.find((todo) => todo.id === id);
      todo.done = !todo.done;
    });
  }, []);

  const handleAdd = useCallback(() => {
    setTodos((draft) => {
      draft.push({
        id: "todo_" + Math.random(),
        title: "A new todo",
        done: false
      });
    });
  }, []);

  // etc

For the full demo see CodeSandbox.

useReducer + Immer

Similarly to useState, useReducer combines neatly with Immer as well, as demonstrated in this CodeSandbox:

javascript
import React, {useCallback, useReducer} from "react"
import {produce} from "immer"

const TodoList = () => {
	const [todos, dispatch] = useReducer(
		produce((draft, action) => {
			switch (action.type) {
				case "toggle":
					const todo = draft.find(todo => todo.id === action.id)
					todo.done = !todo.done
					break
				case "add":
					draft.push({
						id: action.id,
						title: "A new todo",
						done: false
					})
					break
				default:
					break
			}
		}),
		[
			/* initial todos */
		]
	)

	const handleToggle = useCallback(id => {
		dispatch({
			type: "toggle",
			id
		})
	}, [])

	const handleAdd = useCallback(() => {
		dispatch({
			type: "add",
			id: "todo_" + Math.random()
		})
	}, [])

	// etc
}

useImmerReducer

...which again, can be slightly shorted by useImmerReducer from the use-immer package (demo):

javascript
import React, { useCallback } from "react";
import { useImmerReducer } from "use-immer";

const TodoList = () => {
  const [todos, dispatch] = useImmerReducer(
    (draft, action) => {
      switch (action.type) {
        case "toggle":
          const todo = draft.find((todo) => todo.id === action.id);
          todo.done = !todo.done;
          break;
        case "add":
          draft.push({
            id: action.id,
            title: "A new todo",
            done: false
          });
          break;
        default:
          break;
      }
    },
    [ /* initial todos */ ]
  );

  //etc

Redux + Immer

Redux + Immer is extensively covered in the documentation of Redux Toolkit. For Redux without Redux Toolkit, the same trick as applied to useReducer above can be applied: wrap the reducer function with produce, and you can safely mutate the draft!

For example:

javascript
import {produce} from "immer"

// Reducer with initial state
const INITIAL_STATE = [
	/* bunch of todos */
]

const todosReducer = produce((draft, action) => {
	switch (action.type) {
		case "toggle":
			const todo = draft.find(todo => todo.id === action.id)
			todo.done = !todo.done
			break
		case "add":
			draft.push({
				id: action.id,
				title: "A new todo",
				done: false
			})
			break
		default:
			break
	}
})