Example
Without Immer
Do you really want to be doing this…
function reducerSpreadHell(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue,
},
},
},
}
}
One could argue that you should be normalizing your store instead of writing reducers like that, and you’d be right.
With Immer
Still, with immer, all that becomes just this:
state.first.second[action.someId].fourth = action.someValue;
Immer Reducers
You’d write them as if you just didn’t care about immutability!
import { current, PayloadAction } from "@reduxjs/toolkit";
reducers: {
addTodo: (state: WritableDraft<Todo>[], action: PayloadAction<Todo>) => {
// ReduxJS/Toolkit wraps the Todo[] state in a WritableDraft
// "[].push()" does NOT mutate!
// Same for pop, shift, splice etc...
state.push(action.payload);
},
removeTodo: (state, action: PayloadAction<number>) => {
// ATTN: state = newState does NOT work!
// In such case, return the newState
return state.filter(todo => todo.id !== action.payload);
},
// ATTN: Mutate or Return, but not both!
altAddTodo: (state, action: PayloadAction<Todo>) => void state.push(action.payload),
toggleTodo: (state, action: PayloadAction<number>) => {
// If you want to look at the current state, you need to use current()
// There is also original()
console.log('current(state)', current(state));
const todo = state.find(todo => todo.id === action.payload)!;
todo.done = !todo.done;
},
updateTodo: (state, action: PayloadAction<Todo>) => {
const index = state.findIndex(todo => todo.id === action.payload.id);
if (index !== -1) {
state[index] = action.payload;
}
},
},
Caveats
There are some caveats like using current() when you want to look at the state.
Replacing the entire state
As in the case of removeTodo, you cannot completely replace state.
// ⚠️⚠️ DOES NOT WORK ⚠️⚠️
removeTodo: (state, action: PayloadAction<number>) => {
// This could only work if state would be "by ref"
// which JavaScript doesn't have
state = state.filter(todo => todo.id !== action.payload);
}
// ✔️✔️ DOES WORK ✔️✔️
removeTodo: (state, action: PayloadAction<number>) => {
return state.filter(todo => todo.id !== action.payload);
}
Reducers without curly braces
Use void for your one-liner reducers:
// ⚠️⚠️ TypeScript ERROR ⚠️⚠️
// [].push() returns a number which is not a valid new state
addTodo: (state, action: PayloadAction<Todo>) => state.push(action.payload)
// ✔️✔️ BOTH WORK ✔️✔️
addTodo: (state, action: PayloadAction<Todo>) => void state.push(action.payload)
addTodo: (state, action: PayloadAction<Todo>) => {
state.push(action.payload);
}
Primitives cannot be wrapped
If you extract a primitive value (number, bool, string) from the state and then manipulate it, you’re no longer working with a WritableDraft and are just mutating the local variable.
addMessage: (state, action: PayloadAction<any>) => {
// ⚠️⚠️ DOES NOT WORK ⚠️⚠️
let { newMessagesCount } = state;
newMessagesCount++;
// ✔️✔️ DOES WORK ✔️✔️
state.newMessagesCount++;
}
Under the hood
Immer’s bread and butter is the produce(baseState, recipe) function. It takes your baseState, wraps it in a WritableDraft and executes a recipe against it, which means calling your reducer with the dispatched action. It then creates the new state by re-applying the steps done in the recipe to the baseState, but in a non-mutating way.
The ReduxJS/Toolkit reducers have already called produce, so this has been abstracted away for us.

Produce Example
Small code example to illustrate how produce works. In the accompanying git repository, the code is a bit more involved and includes tests to prove that only the objects that changed actually have different references!
import { produce } from "immer";
interface UpdatePizzaTopping {
type: "🍄" | "🌶️" | "🥓";
doubleIt: boolean;
}
const reducerTastyPizza = (baseState: PizzaState, action: UpdatePizzaTopping) => {
const recipe = (draft: WritableDraft<PizzaState>) => {
const topping = draft.toppings.find(topping => topping.type === action.type);
if (!topping)
return;
topping.doubleIt = action.doubleIt;
}
const newState: PizzaState = produce(baseState, recipe);
return newState;
}
Which you would typically just write like so:
const reducerTastyPizza = (state: PizzaState, action: UpdatePizzaTopping) => {
return produce(state, draft => { /* do stuff here! */ });
}
Immer can be useful even if you are not using ReduxJS/Toolkit. Or just outside your reducers – you already have the dependency anyway!
In a form Component, you might have something like
const [title, setTitle] = useState("");
const [text, setText] = useState("");
// If you've got 10 properties, then 10x useState???
Nimmer!
import { produce } from "immer";
const TodoAdd = () => {
const [todo, setTodo] = useState<Todo>(produce(() => ({
title: '',
text: '',
})));
return (
<>
<input
value={todo.title}
onChange={e => setTodo(produce(todo, draft => {draft.title = e.target.value}))}
/>
<textarea
value={todo.text}
onChange={e => setTodo(produce(todo, draft => {draft.text = e.target.value}))}
/>
</>
);
}
Uhoh, setTodo(produce(todo, draft...)), might as well stick with my 10x useState!
You can make this a lot nicer with a helper function, while still keeping everything type-safe:
const updateTodo = <K extends keyof Todo>(key: K, value: Todo[K]) => {
setTodo(produce(todo, draft => {
draft[key] = value;
}));
}
return (
<input value={todo.title} onChange={e => updateTodo('title', e.target.value)} />
)
useImmer
But of course there is a package for that!
immerjs/use-immer: Use immer to drive state with a React hooks
import { useImmer } from "use-immer";
const TodoAdd = () => {
const [todo, setTodo] = useImmer<Todo>({
title: '',
text: '',
});
return (
<>
<input
value={todo.title}
onChange={e => setTodo(draft => draft.title = e.target.value)}
/>
<textarea
value={todo.text}
onChange={e => setTodo(draft => draft.text = e.target.value)}
/>
</>
);
}
use-immer also exposes useImmerReducer to replace your useReducers.