What is Immer
maindraft object, which Immer then uses to produce a new, immutable state through structural sharing. This prevents the need for manual shallow copying (using spread operators ...) at every level of a nested data structure.repository·main·Indexed 12 days ago
https://github.com/immerjs/immerA library for creating the next immutable state by mutating a current draft tree using a proxy-based mechanism. Version 10.0.3-beta includes the produce() function for state transitions, produceWithPatches() for change tracking, and applyPatches() for state updates. It provides utilities like current(), original(), and isDraft(), as well as plugins for Map, Set, and optimized array methods via enableArrayMethods().
draft object, which Immer then uses to produce a new, immutable state through structural sharing. This prevents the need for manual shallow copying (using spread operators ...) at every level of a nested data structure.The performance testing suite includes several key components:
process.env overhead during production bundling.Benchmarks compare the following versions:
immer7-10: Historical Immer versions.immer10Perf: The current development version (references ../dist).vanilla: Pure JavaScript implementations used as a baseline.original or current utilities to perform operations on plain JavaScript objects instead of proxies.When using enableArrayMethods(), callbacks for intercepted methods (filter, find, some, every, and slice) receive base values instead of drafts for performance reasons.
To mutate items returned by these methods, use the result of the method call, which contains drafts.
import {enableArrayMethods, produce} from "immer"
enableArrayMethods()
produce(state, draft => {
draft.items.filter(item => {
// `item` is a base value here, NOT a draft
// Reading works fine:
return item.value > 10
// But direct mutation here won't be tracked:
// item.value = 999 // ❌ Won't affect the draft!
})
// Instead, use the returned result (which contains drafts):
const filtered = draft.items.filter(item => item.value > 10)
filtered[0].value = 999 // ✅ This works - filtered[0] is a draft
})Understanding these core terms helps navigate Immer documentation:
produce.produce; a function that describes how the state should be "mutated".recipe function. It is a proxy to the original state that allows for safe mutations.produce, typically following the pattern (baseState, ...arguments) => resultState.To avoid missing state updates during asynchronous operations, do not hold a draft open across an await. Instead, follow this pattern:
produce to apply the updates to your state.Incorrect Pattern (Anti-pattern):
const draft = createDraft(user)
draft.todos = await (await window.fetch("...")).json()
const loadedUser = finishDraft(draft)Correct Pattern:
const todos = await (await window.fetch("...")).json()
const loadedUser = produce(user, draft => {
draft.todos = todos
})// Correct way: Fetch first, then produce
const todos = await (await window.fetch("http://host/" + user.name)).json();
const loadedUser = produce(user, draft => {
draft.todos = todos;
});Immer patches are similar to the RFC-6902 JSON patch standard, with the key difference that the path property is an array of segments rather than a string.
Example patch structure:
[
{
"op": "replace",
"path": ["profile"],
"value": {"name": "Veria", "age": 5}
},
{
"op": "remove",
"path": ["tags", 3]
}
]To normalize these to the official JSON patch specification, you can join the path array: patch.path = patch.path.join("/").
In Immer, you typically modify the draft object directly. However, you can also replace the entire state by returning a new value from the producer function.
draft, you do not need to return anything. Returning draft is also acceptable but redundant.draft.draft = { ... } does nothing because it only reassigns the local variable, not the actual state.draft (e.g., draft.count += 1) and then returning a new object (e.g., return { count: 2 }) is not allowed and will lead to unexpected behavior.For multiple changes, the 'Immer way' is to mutate the draft directly rather than constructing a new object manually.
const userReducer = produce((draft, action) => {
switch (action.type) {
case "renameUser":
draft.users[action.payload.id].name = action.payload.name
return draft // OK: same as just 'return'
case "loadUsers":
return action.payload // OK: returns an entirely new state
case "adduser-1":
draft = {users: [...draft.users, action.payload]} // NOT OK: reassigning draft does nothing
case "adduser-2":
draft.userCount += 1
return {users: [...draft.users, action.payload]} // NOT OK: modifying draft AND returning new state
case "adduser-4":
draft.userCount += 1
draft.users.push(action.payload) // OK: the immer way (mutating draft)
}
})By default, Immer automatically freezes any state trees that are modified using produce. This mechanism protects your state tree from accidental modifications outside of a producer function.
Key behaviors to note:
freeze utility to shallowly pre-freeze data instead.