Pitfalls

Performance tips#

For performance tips, see Performance Tips.

Don't reassign the recipe argument#

Never reassign the draft argument (example: draft = myCoolNewState). Instead, either modify the draft or return a new state. See Returning data from producers.

Immer only supports unidirectional trees#

Immer assumes your state to be a unidirectional tree. That is, no object should appear twice in the tree, there should be no circular references. There should be exactly one path from the root to any node of the tree.

Never explicitly return undefined from a producer#

It is possible to return values from producers, except, it is not possible to return undefined that way, as it is indistinguishable from not updating the draft at all! If you want to replace the draft with undefined, just return nothing from the producer.

Don't mutate exotic objects#

Immer does not support exotic objects such as window.location.

Classes should be made draftable or not mutated#

You will need to enable your own classes to work properly with Immer. For docs on the topic, check out the section on working with complex objects.

Only valid indices and length can be mutated on Arrays#

For arrays, only numeric properties and the length property can be mutated. Custom properties are not preserved on arrays.

Data not originating from the state will never be drafted#

Note that data that comes from the closure, and not from the base state, will never be drafted, even when the data has become part of the new draft.

function onReceiveTodo(todo) {
const nextTodos = produce(todos, draft => {
draft.todos[todo.id] = todo
// Note, because 'todo' is coming from external, and not from the 'draft',
// it isn't draft so the following modification affects the original todo!
draft.todos[todo.id].done = true
// The reason for this, is that it means that the behavior of the 2 lines above
// is equivalent to code, making this whole process more consistent
todo.done = true
draft.todos[todo.id] = todo
})
}

Immer patches are not necessarily optimal#

The set of patches generated by Immer should be correct, that is, applying them to an equal base object should result in the same end state. However Immer does not guarantee the generated set of patches will be optimal, that is, the minimum set of patches possible.

Always use the result of nested producers#

Nested produce calls are supported, but note that produce will always produce a new state, so even when passing a draft to a nested produce, the changes made by the inner produce won't be visible in the draft that was passed it, but only in the output that is produced. In other words, when using nested produce, you get a draft of a draft and the result of the inner produce should be merged back into the original draft (or returned). For example produce(state, draft => { produce(draft.user, userDraft => { userDraft.name += "!" })}) won't work as the output if the inner produce isn't used. The correct way to use nested producers is:

produce(state, draft => {
draft.user = produce(draft.user, userDraft => {
userDraft.name += "!"
})
})

Drafts aren't referentially equal#

Draft objects in Immer are wrapped in Proxy, so you cannot use == or === to test equality between an original object and its equivalent draft (eg. when matching a specific element in an array). Instead, you can use the original helper:

const remove = produce((list, element) => {
const index = list.indexOf(element) // this won't work!
const index = original(list).indexOf(element) // do this instead
if (index > -1) list.splice(index, 1)
})
const values = [a, b, c]
remove(values, a)

If possible, it's recommended to perform the comparison outside the produce function, or to use a unique identifier property like .id instead, to avoid needing to use original.