Technical Documentation: immerjs/immer
โน๏ธ Provenance: Multi-Tier Ingestion ยท CodeWiki Reference
Overview (`readme.md`)
Immer
Create the next immutable state tree by simply modifying the current tree
Winner of the "Breakthrough of the year" React open source award and "Most impactful contribution" JavaScript open source award in 2019
Contribute using one-click online setup
You can use Gitpod (a free online VSCode like IDE) for contributing online. With a single click it will launch a workspace and automatically:
- clone the immer repo.
- install the dependencies.
- run
yarn run start.
so that you can start coding straight away.
Documentation
The documentation of this package is hosted at https://immerjs.github.io/immer/
Support
Did Immer make a difference to your project? Join the open collective at https://opencollective.com/immer!
Release notes
https://github.com/immerjs/immer/releases
Overview (`perf-testing/README.md`)
Immer Performance Testing
This directory contains performance testing tools for Immer, allowing you to benchmark different versions of Immer and analyze CPU profiles to identify performance bottlenecks.
Setup
Install dependencies (in this directory):
bashyarn installBuild Immer first:
bashyarn build-immerBuild the benchmark bundle:
bashyarn build
Alternately, you can rebuild both Immer and the benchmarking script:
yarn build-with-latestUsage
Running Benchmarks
To run the performance benchmarks:
# Run basic benchmarks, with relative version speed comparisons
yarn benchmark
# Run the benchmarks, but also generate a CPU profile
yarn profileAnalyzing CPU Profiles
After running yarn profile, you'll get a .cpuprofile file. To analyze it:
# Analyze the most recent profile
yarn analyze-profile your-profile.cpuprofileWhat's Included
- immutability-benchmarks.mjs: Main benchmark script comparing different Immer versions
- read-cpuprofile.js: Advanced CPU profile analyzer with sourcemap support
- rolldown.config.js: Bundler configuration that eliminates
process.envoverhead
Benchmark Versions
The benchmarks compare:
- immer7-10: Historical Immer versions
- immer10Perf: Current development version (references
../dist) - vanilla: Pure JavaScript implementations for baseline comparison
Key Features
- Sourcemap support: CPU profile analysis includes original function names
- Version-aware analysis: Breaks down performance by Immer version
- Production bundling: Uses Rolldown to eliminate development overhead
Api (`website/docs/api.md`)
id: api title: API overview
| Exported name | Description | Section |
|---|---|---|
produce |
The core API of Immer: import {produce} from "immer" |
Produce |
applyPatches |
Given a base state or draft, and a set of patches, applies the patches | Patches |
castDraft |
Converts any immutable type to its mutable counterpart. This is just a cast and doesn't actually do anything. | TypeScript |
castImmutable |
Converts any mutable type to its immutable counterpart. This is just a cast and doesn't actually do anything. | TypeScript |
createDraft |
Given a base state, creates a mutable draft for which any modifications will be recorded | Async |
current |
Given a draft object (doesn't have to be a tree root), takes a snapshot of the current state of the draft | Current |
Draft<T> |
Exposed TypeScript type to convert an immutable type to a mutable type | TypeScript |
enableArrayMethods() |
Enables optimized array method handling for improved performance with array-heavy operations. | Array Methods |
enableMapSet() |
Enables support for Map and Set collections. |
Installation |
enablePatches() |
Enables support for JSON patches. | Installation |
finishDraft |
Given an draft created using createDraft, seals the draft and produces and returns the next immutable state that captures all the changes |
Async |
freeze(obj, deep?) |
Freezes draftable objects. Returns the original object. By default freezes shallowly, but if the second argument is true it will freeze recursively. |
|
Immer |
constructor that can be used to create a second "immer" instance (exposing all APIs listed in this instance), that doesn't share its settings with global instance. | |
immerable |
Symbol that can be added to a constructor or prototype, to indicate that Immer should treat the class as something that can be safely drafted | Classes |
Immutable<T> |
Exposed TypeScript type to convert mutable types to immutable types | |
isDraft |
Returns true if the given object is a draft object | |
isDraftable |
Returns true if Immer is capable of turning this object into a draft. Which is true for: arrays, objects without prototype, objects with Object as their prototype, objects that have the immerable symbol on their constructor or prototype |
|
nothing |
Value that can be returned from a recipe, to indicate that the value undefined should be produced |
Return |
original |
Given a draft object (doesn't have to be a tree root), returns the original object at the same path in the original state tree, if present | Original |
Patch |
Exposed TypeScript type, describes the shape of an (inverse) patch object | Patches |
produceWithPatches |
Works the same as produce, but instead of just returning the produced object, it returns a tuple, consisting of [result, patches, inversePatches]. |
Patches |
setAutoFreeze |
Enables / disables automatic freezing of the trees produces. By default enabled. | Freezing |
setUseStrictShallowCopy |
Can be used to enable strict shallow copy. If enable, immer copies non-enumerable properties as much as possible. | Classes |
setUseStrictIteration |
Controls iteration behavior: pass false for loose iteration (enumerable properties only, better performance) or true for strict iteration (includes symbols and non-enumerable properties). By default disabled (loose iteration). |
Importing immer
In most cases, the only thing you need to import from Immer is produce:
import {produce} from "immer"Note that in older versions, produce was also available as default export (e.g. import produce from "immer" was also valid, but that is no longer the case to improve eco system compatibility.
Array Methods (`website/docs/array-methods.md`)
id: array-methods title: Array Methods Plugin
Overview
The Array Methods Plugin (enableArrayMethods()) optimizes array operations within Immer producers by avoiding unnecessary Proxy creation during iteration. This provides significant performance improvements for array-heavy operations.
Why does this matter? Without the plugin, every array element access during iteration (e.g., in filter, find, slice) creates a Proxy object. For a 1000-element array, this means 1000+ proxy trap invocations just to iterate. With the plugin enabled, callbacks receive base (non-proxied) values, and proxies are only created as needed for mutation tracking.
Installation
Enable the plugin once at your application's entry point:
import {enableArrayMethods} from "immer"
enableArrayMethods()This adds approximately 2KB to your bundle size.
Mutating Methods
These methods modify the array in-place and operate directly on the draft's internal copy without creating per-element proxies:
| Method | Returns | Description |
|---|---|---|
push() |
New length | Adds elements to the end |
pop() |
Removed element | Removes and returns the last element |
shift() |
Removed element | Removes and returns the first element |
unshift() |
New length | Adds elements to the beginning |
splice() |
Removed elements | Adds/removes elements at any position |
sort() |
The draft array | Sorts elements in place |
reverse() |
The draft array | Reverses the array in place |
import {produce, enableArrayMethods} from "immer"
enableArrayMethods()
const base = {items: [3, 1, 4, 1, 5]}
const result = produce(base, draft => {
draft.items.push(9) // Adds 9 to end
draft.items.sort() // Sorts: [1, 1, 3, 4, 5, 9]
draft.items.reverse() // Reverses: [9, 5, 4, 3, 1, 1]
})Non-Mutating Methods
Non-mutating methods are categorized based on what they return:
Subset Operations (Return Drafts)
These methods select items that exist in the original array and create draft proxies for the returned items. The callbacks receive base values (the optimization), but the returned array contains newly created draft proxies that point back to the original positions. Mutations to returned items WILL affect the draft state.
| Method | Returns | Drafts? |
|---|---|---|
filter() |
Array of matching items | โ Yes |
slice() |
Array of items in range | โ Yes |
find() |
First matching item or undefined |
โ Yes |
findLast() |
Last matching item or undefined |
โ Yes |
const base = {
items: [
{id: 1, value: 10},
{id: 2, value: 20},
{id: 3, value: 30}
]
}
const result = produce(base, draft => {
// filter returns drafts - mutations track back to original
const filtered = draft.items.filter(item => item.value > 15)
filtered[0].value = 999 // This WILL affect draft.items[1]
// find returns a draft - mutations track back
const found = draft.items.find(item => item.id === 3)
if (found) {
found.value = 888 // This WILL affect draft.items[2]
}
// slice returns drafts
const sliced = draft.items.slice(0, 2)
sliced[0].value = 777 // This WILL affect draft.items[0]
})
console.log(result.items[0].value) // 777
console.log(result.items[1].value) // 999
console.log(result.items[2].value) // 888Transform Operations (Return Base Values)
These methods create new arrays that may include external items or restructured data. They return base values, NOT drafts. Mutations to returned items will NOT track back to the draft state.
| Method | Returns | Drafts? |
|---|---|---|
concat() |
New combined array | โ No |
flat() |
New flattened array | โ No |
const base = {items: [{id: 1, value: 10}]}
const result = produce(base, draft => {
// concat returns base values - mutations DON'T track
const concatenated = draft.items.concat([{id: 2, value: 20}])
concatenated[0].value = 999 // This will NOT affect draft.items[0]
// To actually use concat results, assign them:
draft.items = draft.items.concat([{id: 2, value: 20}])
})
// Original unchanged because concat result wasn't assigned
console.log(result.items[0].value) // 10 (unchanged)Why the distinction?
- Subset operations (
filter,slice,find) select items that exist in the original array. Returning drafts allows mutations to propagate back to the source. - Transform operations (
concat,flat) create new data structures that may include external items or restructured data, making draft tracking impractical.
Primitive-Returning Methods
These methods return primitive values (numbers, booleans, strings). No tracking issues since primitives aren't draftable:
| Method | Returns |
|---|---|
indexOf() |
Number (index or -1) |
lastIndexOf() |
Number (index or -1) |
includes() |
Boolean |
some() |
Boolean |
every() |
Boolean |
findIndex() |
Number (index or -1) |
findLastIndex() |
Number (index or -1) |
join() |
String |
toString() |
String |
toLocaleString() |
String |
const base = {
items: [
{id: 1, active: true},
{id: 2, active: false}
]
}
const result = produce(base, draft => {
const index = draft.items.findIndex(item => item.id === 2)
const hasActive = draft.items.some(item => item.active)
const allActive = draft.items.every(item => item.active)
console.log(index) // 1
console.log(hasActive) // true
console.log(allActive) // false
})Methods NOT Overridden
The following methods are not intercepted by the plugin and work through standard Proxy behavior. Callbacks receive drafts, and mutations track normally:
| Method | Description |
|---|---|
map() |
Transform each element |
flatMap() |
Map then flatten |
forEach() |
Execute callback for each element |
reduce() |
Reduce to single value |
reduceRight() |
Reduce from right to left |
const base = {
items: [
{id: 1, value: 10, nested: {count: 0}},
{id: 2, value: 20, nested: {count: 0}}
]
}
const result = produce(base, draft => {
// forEach receives drafts - mutations work normally
draft.items.forEach(item => {
item.value *= 2
})
// map is NOT overridden - callbacks receive drafts
// The returned array items are also drafts (extracted from draft.items)
const mapped = draft.items.map(item => item.nested)
// Mutations to the result array propagate back
mapped[0].count = 999 // โ
This affects draft.items[0].nested.count
})
console.log(result.items[0].nested.count) // 999Callback Behavior
For overridden methods, callbacks receive base values (not drafts). This is the core optimization - it avoids creating proxies for every element during iteration.
const base = {
items: [
{id: 1, value: 10},
{id: 2, value: 20}
]
}
produce(base, draft => {
draft.items.filter(item => {
// `item` is a base value here, NOT a draft
// Reading properties works fine
return item.value > 15
// But direct mutation here won't be tracked:
// item.value = 999 // โ Won't affect draft
})
// Instead, use the returned draft:
const filtered = draft.items.filter(item => item.value > 15)
filtered[0].value = 999 // โ
This works because filtered[0] is a draft
})Method Return Behavior Summary
| Category | Methods | Returns | Mutations Track? |
|---|---|---|---|
| Subset | filter, slice, find, findLast |
Draft proxies | โ Yes |
| Transform | concat, flat |
Base values | โ No |
| Primitive | indexOf, includes, some, every, findIndex, findLastIndex, lastIndexOf, join, toString, toLocaleString |
Primitives | N/A |
| Mutating | push, pop, shift, unshift, splice, sort, reverse |
Various | โ Yes (modifies draft) |
| Not Overridden | map, flatMap, forEach, reduce, reduceRight |
Standard behavior | โ Yes (callbacks get drafts) |
When to Use
Enable the Array Methods Plugin when:
- Your application has significant array iteration within producers
- You frequently use methods like
filter,find,some,everyon large arrays - Performance profiling shows array operations as a bottleneck
The plugin is most beneficial for:
- Large arrays (100+ elements)
- Frequent producer calls with array operations
- Read-heavy operations (filtering, searching) where most elements aren't modified
Performance Benefit
Without the plugin:
- Every array element access during iteration creates a Proxy
- A
filter()on 1000 elements = 1000+ proxy creations
With the plugin:
- Callbacks receive base values directly
- Proxies only created for the specific elements you actually mutate, or that match filtering predicates
// Without plugin: ~3000+ proxy trap invocations
// With plugin: ~10-20 proxy trap invocations
const result = produce(largeState, draft => {
const filtered = draft.items.filter(x => x.value > threshold)
// Only items you mutate get proxied
filtered.forEach(item => {
item.processed = true
})
})Async (`website/docs/async.mdx`)
id: async title: createDraft / finishDraft sidebar_label: createDraft / finishDraft
egghead.io lesson 11: Creating **async** producers (and why you
shouldnโt)
<a
className="egghead-link"
href="https://egghead.io/lessons/react-write-asynchronous-producers-in-immer-and-why-you-shouldn-t"
>
Hosted on egghead.io
</a>`createDraft` and `finishDraft`
createDraft and finishDraft are two low-level functions that are mostly useful for libraries that build abstractions on top of immer. It avoids the need to always create a function in order to work with drafts. Instead, one can create a draft, modify it, and at some time in the future finish the draft, in which case the next immutable state will be produced.
Beyond that, createDraft / finishDraft could be used to express async updates to drafts:
import {createDraft, finishDraft} from "immer"
const user = {
name: "michel",
todos: []
}
const draft = createDraft(user)
draft.todos = await (await window.fetch("http://host/" + draft.name)).json()
const loadedUser = finishDraft(draft)Note: The above is an anti-pattern! First fetch data instead, then draft the user. Otherwise updates to user that happen during the async process, would be "missed" by the draft.
Note: finishDraft takes a patchListener as second argument, which can be used to record the patches, similarly to produce.
Warning: in general, we recommend to use produce instead of the createDraft / finishDraft combo, produce is less error prone in usage, and more clearly separates the concepts of mutability and immutability in your code base.
Built With (`website/docs/built-with.md`)
id: built-with title: Built with Immer
- react-copy-write Immutable state with a mutable API
- redux-toolkit The official, opinionated, batteries-included toolset for efficient Redux development
- immer based handleActions Boilerplate free actions for Redux
- redux-box Modular and easy-to-grasp redux based state management, with least boilerplate
- quick-redux tools to make redux development quicker and easier
- bey Simple immutable state for React using Immer
- cool-store CoolStore is an immutable state store built on top of ImmerJS and RxJS
- immer-wieder State management lib that combines React 16 Context and immer for Redux semantics
- robodux flexible way to reduce redux boilerplate
- immer-reducer Type-safe and terse React (useReducer()) and Redux reducers with Typescript
- redux-ts-utils Everything you need to create type-safe applications with Redux with a strong emphasis on simplicity
- react-state-tree Drop-in replacement for useState that persists your state into a redux-like state tree
- redux-immer is used to create an equivalent function of Redux combineReducers that works with
immerstate. Likeredux-immutablebut forimmer - ngrx-wieder Lightweight yet configurable solution for implementing undo-redo in Angular apps on top of NgRx and Immer
- immer-yjs Combine
immerwith CRDT libraryy.jsfor easy json data manipulation - immerhin Sync state with undo/redo
- zenith Reactive React state management on Immer with MobX-style computed properties and patch-based undo/redo, powering DOMD.
- domd High-performance macOS markdown editor built on Immer's structural sharing, with near-O(1) per-keystroke rendering on large documents.
- ... and many more
Complex Objects (`website/docs/complex-objects.md`)
id: complex-objects title: Classes
Plain objects (objects without a prototype), arrays, Maps and Sets are always drafted by Immer. Every other object must use the immerable symbol to mark itself as compatible with Immer. When one of these objects is mutated within a producer, its prototype is preserved between copies.
import {immerable} from "immer"
class Foo {
[immerable] = true // Option 1
constructor() {
this[immerable] = true // Option 2
}
}
Foo[immerable] = true // Option 3Example
import {immerable, produce} from "immer"
class Clock {
[immerable] = true
constructor(hour, minute) {
this.hour = hour
this.minute = minute
}
get time() {
return `${this.hour}:${this.minute}`
}
tick() {
return produce(this, draft => {
draft.minute++
})
}
}
const clock1 = new Clock(12, 10)
const clock2 = clock1.tick()
console.log(clock1.time) // 12:10
console.log(clock2.time) // 12:11
console.log(clock2 instanceof Clock) // trueSemantics in detail
The semantics on how classes are drafted are as follows:
- A draft of a class is a fresh object but with the same prototype as the original object.
- When creating a draft, Immer will copy all own properties from the base to the draft.This includes (in strict mode) non-enumerable and symbolic properties.
- Own getters will be invoked during the copy process, just like
Object.assignwould. - Inherited getters and methods will remain as is and be inherited by the draft, as they are stored on the prototype which is untouched.
- Immer will not invoke constructor functions.
- The final instance will be constructed with the same mechanism as the draft was created.
- Only getters that have a setter as well will be writable in the draft, as otherwise the value can't be copied back.
Because Immer will dereference own getters of objects into normal properties, it is possible to use objects that use getter/setter traps on their fields, like MobX and Vue do.
Note that, by default, Immer does not strictly handle object's non-enumerable properties such as getters/setters for performance reason. If you want this behavior to be strict, you can opt-in with useStrictShallowCopy(config). Use true to always copy strict, or "class_only" to only copy class instances strictly but use the faster loose copying for plain objects. The default is false. (Remember, regardless of strict mode, own getters / setters are always copied by value. There is currently no config to copy descriptors as-is. Feature request / PR welcome).
Immer does not support exotic / engine native objects such as DOM Nodes or Buffers, nor is subclassing Map, Set or arrays supported and the immerable symbol can't be used on them.
So when working for example with Date objects, you should always create a new Date instance instead of mutating an existing Date object.
Current (`website/docs/current.md`)
id: current title: Extracting the current state from a draft sidebar_label: Current
Immer exposes a named export current that creates a copy of the current state of the draft. This can be very useful for debugging purposes (as those objects won't be Proxy objects and not be logged as such). Also, references to current can be safely leaked from a produce function. Put differently, current provides a snapshot of the current state of a draft.
Objects generated by current work similar to the objects created by produce itself.
- Unmodified objects will be structurally shared with the original objects.
- If no changes are made to a draft, generally it holds that
original(draft) === current(draft), but this is not guaranteed. - Future changes to the draft won't be reflected in the object produced by
current(except for references to undraftable objects) - Unlike
produceobjects created bycurrentwill not be frozen.
Use current sparingly, it can be a potentially expensive operation, especially when using ES5.
Note that current cannot be invoked on objects that aren't drafts.
Example
The following example shows the effect of current (and original):
const base = {
x: 0
}
const next = produce(base, draft => {
draft.x++
const orig = original(draft)
const copy = current(draft)
console.log(orig.x)
console.log(copy.x)
setTimeout(() => {
// this will execute after the produce has finished!
console.log(orig.x)
console.log(copy.x)
}, 100)
draft.x++
console.log(draft.x)
})
console.log(next.x)
// This will print
// 0 (orig.x)
// 1 (copy.x)
// 2 (draft.x)
// 2 (next.x)
// 0 (after timeout, orig.x)
// 1 (after timeout, copy.x)Curried Produce (`website/docs/curried-produce.mdx`)
id: curried-produce title: Curried producers
egghead.io lesson 6: Simplify code by using curried _reduce_
<a
className="egghead-link"
href="https://egghead.io/lessons/javascript-simplify-immer-producer-functions-using-currying"
>
Hosted on egghead.io
</a>Passing a function as the first argument to produce creates a function that doesn't apply produce yet to a specific state, but rather creates a function that will apply produce to any state that is passed to it in the future. This generally is called currying. Take for example the following example:
import {produce} from "immer"
function toggleTodo(state, id) {
return produce(state, draft => {
const todo = draft.find(todo => todo.id === id)
todo.done = !todo.done
})
}
const baseState = [
{
id: "JavaScript",
title: "Learn TypeScript",
done: true
},
{
id: "Immer",
title: "Try Immer",
done: false
}
]
const nextState = toggleTodo(baseState, "Immer")The above pattern of toggleTodo is quite typical; pass an existing state to produce, modify the draft, and then return the result. Since state isn't used for anything else than passing it on to produce, the above example can be simplified by using the curried form of produce, where you pass produce only the recipe function, and produce will return a new function that will apply recipe to the base state. This allows us to shorten the above toggleTodo definition.
import {produce} from "immer"
// curried producer:
const toggleTodo = produce((draft, id) => {
const todo = draft.find(todo => todo.id === id)
todo.done = !todo.done
})
const baseState = [
/* as is */
]
const nextState = toggleTodo(baseState, "Immer")Note that the id param has now become part of the recipe function! This pattern of having curried producers combines really neatly with for example the useState hook from React, as we will see on the next page.
Example Setstate (`website/docs/example-setstate.mdx`)
id: example-setstate title: React & Immer
egghead.io lesson 8: Using Immer with _useState_. Or: _useImmer_
<a
className="egghead-link"
href="https://egghead.io/lessons/react-immutable-update-state-inside-react-components-with-useimmer"
>
Hosted on egghead.io
</a>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.
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>{/* See CodeSandbox */}</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:
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
});
});
}, []);
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<label>
<input
type="checkbox"
checked={todo.done}
onChange={() => handleToggle(todo.id)}
/>
{todo.title}
</label>
</li>
))}
<button type="button" onClick={handleAdd}>
Add todo
</button>
</ul>
);
};useReducer + Immer
Similarly to useState, useReducer combines neatly with Immer as well, as demonstrated in this CodeSandbox:
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):
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 */ ]
);
//etcRedux + 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:
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
}
})Faq (`website/docs/faq.md`)
id: faq title: Frequently Asked Questions sidebar_label: FAQ
Q: How does Immer work?
Read the (second part of the) introduction blog.
Q: Does Immer use structural sharing? So that my selectors can be memoized and such?
A: Yes
Q: Does Immer support deep updates?
A: Yes
Q: I can't rely on Proxies being present on my target environments. Can I use Immer?
A: Yes - view details
Q: Can I typecheck my data structures when using Immer?
A: Yes
Q: Can I store `Date` objects, functions etc in my state tree when using Immer?
A: Yes
Q: Can I use Maps and Sets?
A: Yes
Q: Is it fast?
A: Yes
Q: Idea! Can Immer freeze the state for me?
A: Yes
Freezing (`website/docs/freezing.mdx`)
id: freezing title: Auto freezing
egghead.io lesson 7: Immer automatically freezes data
<a
className="egghead-link"
href="https://egghead.io/lessons/javascript-produces-immutable-data-and-avoid-unnecessary-creation-of-new-data-trees-with-immer"
>
Hosted on egghead.io
</a>Immer automatically freezes any state trees that are modified using produce. This protects against accidental modifications of the state tree outside of a producer. In most cases this provides the most optimal behavior, but setAutoFreeze(true / false) can be used to explicitly turn this feature on or off.
Immer will never freeze (the contents of) non-enumerable, non-own or symbolic properties, unless their content was drafted.
โ ๏ธ Immer freezes everything recursively, for large data objects that won't be changed in the future this might be over-kill, in that case it can be more efficient to shallowly pre-freeze data using the freeze utility.โ ๏ธ
โ ๏ธ If auto freezing is enabled, recipes are not entirely side-effect free: Any plain object or array that ends up in the produced result, will be frozen, even when these objects were not frozen before the start of the producer! โ ๏ธ
Installation (`website/docs/installation.mdx`)
id: installation title: Installation
Immer can be installed as a direct dependency, and will work in any ES5 environment:
- Yarn:
yarn add immer - NPM:
npm install immer - Deno:
deno install immer - CDN: Exposed global is
immer- Unpkg:
<script src="https://unpkg.com/immer"></script> - JSDelivr:
<script src="https://cdn.jsdelivr.net/npm/immer"></script> - โ ๏ธ When using a CDN, it is best to check the url in your browser and see what version it resolves to, so that your users aren't accidentally served a newer version in the future when updates are release. So use a url like: https://unpkg.com/[email protected]/dist/immer.umd.production.min.js instead. Substitute
production.minwithdevelopmentin the URL for a development build.
- Unpkg:
Pick your Immer version
This section only applies to version 6 and later
To make sure Immer is as small as possible, features that are not required by every project has been made opt-in, and have to be enabled explicitly. This ensures that when bundling your application for production, unused features don't take any space.
The following features can be opt-in to:
| Feature | Description | Method to call |
|---|---|---|
| Array Methods optimization | Optimizes array method handling for improved performance with array-heavy operations | enableArrayMethods() |
| ES2015 Map and Set support | To enable Immer to operate on the native Map and Set collections, enable this feature |
enableMapSet() |
| JSON Patch support | Immer can keep track of all the changes you make to draft objects. This can be useful for communicating changes using JSON patches | enablePatches() |
For example, if you want to use produce on a Map, you need to enable this feature once during the start of your application:
// In your application's entrypoint
import {enableMapSet} from "immer"
enableMapSet()
// ...later
import {produce} from "immer"
const usersById_v1 = new Map([
["michel", {name: "Michel Weststrate", country: "NL"}]
])
const usersById_v2 = produce(usersById_v1, draft => {
draft.get("michel").country = "UK"
})
expect(usersById_v1.get("michel").country).toBe("NL")
expect(usersById_v2.get("michel").country).toBe("UK")Vanilla Immer kicks in at ~3KB gzipped. Every plugin that is enabled adds ~1-2 KB to that. The breakdown is as follows:
Import size report for immer:
โโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโฌโโโโโโโโโโโโโฌโโโโโโโโโโโโ
โ (index) โ just this โ cumulative โ increment โ
โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโผโโโโโโโโโโโโโผโโโโโโโโโโโโค
โ import * from 'immer' โ 6908 โ 0 โ 0 โ
โ produce โ 4183 โ 4183 โ 0 โ
โ enableMapSet โ 4971 โ 4980 โ 797 โ
โ enablePatches โ 5335 โ 6097 โ 1117 โ
โ enableArrayMethods โ 4768 โ 6659 โ 562 โ
โโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโดโโโโโโโโโโโโโดโโโโโโโโโโโโ
(this report was generated by npmjs.com/package/import-size)Immer on older JavaScript environments?
By default produce tries to use proxies for optimal performance. However, on older JavaScript engines Proxy is not available. For example, when running Microsoft Internet Explorer or React Native (if < v0.59 or when using the Hermes engine on React Native < 0.64) on Android. In such cases, Immer will fallback to an ES5 compatible implementation which works identically, but is a bit slower.
- Since version 6, support for the fallback implementation has to be explicitly enabled by calling
enableES5(). - Version 10 drops the fallback implementation fully, and cannot be used in browsers / engines that don't support Proxy.
Introduction (`website/docs/introduction.md`)
id: introduction title: Introduction to Immer sidebar_label: Introduction slug: /
Immer
Immer (German for: always) is a tiny package that allows you to work with immutable state in a more convenient way.
Immer is life-changing as a JS dev, and I'm not even exaggerating :) Like, it's right up there with Prettier in terms of "wow this package is amazing, how did I ever live without it?" --Mark Erikson, (the) Redux Maintainer, @replayio
Winner of the "Breakthrough of the year" React open source award and "Most impactful contribution" JavaScript open source award in 2019.
- Introduction blogpost: Immer: Immutability the easy way
- Short Egghead.io lesson covering the Immer essentials: Simplify creating immutable data trees with Immer (7m)
- Free in-depth Egghead.io course: Immutable JavaScript Data Structures with Immer (58m)
Immer simplifies handling immutable data structures
Immer can be used in any context in which immutable data structures need to be used. For example in combination with React state, React or Redux reducers, or configuration management. Immutable data structures allow for (efficient) change detection: if the reference to an object didn't change, the object itself did not change. In addition, it makes cloning relatively cheap: Unchanged parts of a data tree don't need to be copied and are shared in memory with older versions of the same state.
Generally speaking, these benefits can be achieved by making sure you never change any property of an object, array or map, but by always creating an altered copy instead. In practice this can result in code that is quite cumbersome to write, and it is easy to accidentally violate those constraints. Immer will help you to follow the immutable data paradigm by addressing these pain points:
- Immer will detect accidental mutations and throw an error.
- Immer will remove the need for the typical boilerplate code that is needed when creating deep updates to immutable objects: Without Immer, object copies need to be made by hand at every level. Typically by using a lot of
...spread operations. When using Immer, changes are made to adraftobject, that records the changes and takes care of creating the necessary copies, without ever affecting the original object. - When using Immer, you don't need to learn dedicated APIs or data structures to benefit from the paradigm. With Immer you'll use plain JavaScript data structures, and use the well-known mutable JavaScript APIs, but safely.
A quick example for comparison
const baseState = [
{
title: "Learn TypeScript",
done: true
},
{
title: "Try Immer",
done: false
}
]Imagine we have the above base state, and we'll need to update the second todo, and add a third one. However, we don't want to mutate the original baseState, and we want to avoid deep cloning as well (to preserve the first todo).
Without Immer
Without Immer, we'll have to carefully shallow copy every level of the state structure that is affected by our change:
const nextState = baseState.slice() // shallow clone the array
nextState[1] = {
// replace element 1...
...nextState[1], // with a shallow clone of element 1
done: true // ...combined with the desired update
}
// since nextState was freshly cloned, using push is safe here,
// but doing the same thing at any arbitrary time in the future would
// violate the immutability principles and introduce a bug!
nextState.push({title: "Tweet about it"})With Immer
With Immer, this process is more straightforward. We can leverage the produce function, which takes as first argument the state we want to start from, and as second argument we pass a function, called the recipe, that is passed a draft to which we can apply straightforward mutations. Those mutations are recorded and used to produce the next state once the recipe is done. produce will take care of all the necessary copying, and protect against future accidental modifications as well by freezing the data.
import {produce} from "immer"
const nextState = produce(baseState, draft => {
draft[1].done = true
draft.push({title: "Tweet about it"})
})Looking for Immer in combination with React? Feel free to skip ahead to the React + Immer page.
How Immer works
The basic idea is that with Immer you will apply all your changes to a temporary draft, which is a proxy of the currentState. Once all your mutations are completed, Immer will produce the nextState based on the mutations to the draft state. This means that you can interact with your data by simply modifying it while keeping all the benefits of immutable data.
Using Immer is like having a personal assistant. The assistant takes a letter (the current state) and gives you a copy (draft) to jot changes onto. Once you are done, the assistant will take your draft and produce the real immutable, final letter for you (the next state).
Head to the next section to further dive into produce.
Benefits
- Follow the immutable data paradigm, while using normal JavaScript objects, arrays, Sets and Maps. No new APIs or "mutation patterns" to learn!
- Strongly typed, no string based paths selectors etc.
- Structural sharing out of the box
- Object freezing out of the box
- Deep updates are a breeze
- Boilerplate reduction. Less noise, more concise code.
- First class support for JSON patches
- Small: 3KB gzipped
Map Set (`website/docs/map-set.md`)
id: map-set title: Map and Set
โ Since version 6 support for Maps and Sets has to be enabled explicitly by calling enableMapSet() once when starting your application.
Plain objects, arrays, Maps and Sets are always drafted by Immer. An example of using Maps with immer:
test("Producers can update Maps", () => {
const usersById_v1 = new Map()
const usersById_v2 = produce(usersById_v1, draft => {
// Modifying a map results in a new map
draft.set("michel", {name: "Michel Weststrate", country: "NL"})
})
const usersById_v3 = produce(usersById_v2, draft => {
// Making a change deep inside a map, results in a new map as well!
draft.get("michel").country = "UK"
})
// We got a new map each time!
expect(usersById_v2).not.toBe(usersById_v1)
expect(usersById_v3).not.toBe(usersById_v2)
// With different content obviously
expect(usersById_v1).toMatchInlineSnapshot(`Map {}`)
expect(usersById_v2).toMatchInlineSnapshot(`
Map {
"michel" => Object {
"country": "NL",
"name": "Michel Weststrate",
},
}
`)
expect(usersById_v3).toMatchInlineSnapshot(`
Map {
"michel" => Object {
"country": "UK",
"name": "Michel Weststrate",
},
}
`)
// The old one was never modified
expect(usersById_v1.size).toBe(0)
// And trying to change a Map outside a producers is going to: NO!
expect(() => usersById_v3.clear()).toThrowErrorMatchingInlineSnapshot(
`"This object has been frozen and should not be mutated"`
)
})Maps and Sets that are produced by Immer will be made artificially immutable. This means that they will throw an exception when trying mutative methods like set, clear etc. outside a producer.
Note: The keys of a map are never drafted! This is done to avoid confusing semantics and keep keys always referentially equal
Original (`website/docs/original.md`)
id: original title: Extracting the original state from a draft sidebar_label: Original
Immer exposes a named export original that will get the original object from the proxied instance inside produce (or return undefined for unproxied values). A good example of when this can be useful is when searching for nodes in a tree-like state using strict equality.
import {original, produce} from "immer"
const baseState = {users: [{name: "Richie"}]}
const nextState = produce(baseState, draftState => {
original(draftState.users) // is === baseState.users
})Just want to know if a value is a proxied instance? Use the isDraft function! Note that original cannot be invoked on objects that aren't drafts.
import {isDraft, produce} from "immer"
const baseState = {users: [{name: "Bobby"}]}
const nextState = produce(baseState, draft => {
isDraft(draft) // => true
isDraft(draft.users) // => true
isDraft(draft.users[0]) // => true
})
isDraft(nextState) // => falseOther Lang (`website/docs/other-lang.md`)
id: other-lang title: Porting to other languages
Immer has been ported to other programming languages.
| Language | Link |
|---|---|
| Java | Jimmer |
Patches (`website/docs/patches.mdx`)
id: patches title: Patches
egghead.io lesson 14: Capture patches using _produceWithPatches_
<a
className="egghead-link"
href="https://egghead.io/lessons/react-capture-patches-to-distribute-changes-in-app-state-with-immer-producewithpatches"
>
Hosted on egghead.io
</a>
egghead.io lesson 16: Apply Patches using _applyPatches_
<a
className="egghead-link"
href="https://egghead.io/lessons/react-apply-patches-using-immer-applypatches-to-synchronize-state-across-clients"
>
Hosted on egghead.io
</a>โ Since version 6 support for Patches has to be enabled explicitly by calling enablePatches() once when starting your application.
During the run of a producer, Immer can record all the patches that would replay the changes made by the reducer. This is a very powerful tool if you want to fork your state temporarily and replay the changes to the original.
Patches are useful in few scenarios:
- To exchange incremental updates with other parties, for example over websockets
- For debugging / traces, to see precisely how state is changed over time
- As basis for undo/redo or as an approach to replay changes on a slightly different state tree
To help with replaying patches, applyPatches comes in handy. Here is an example how patches could be used to record the incremental updates and (inverse) apply them:
import {produce, applyPatches} from "immer"
// version 6
import {enablePatches} from "immer"
enablePatches()
let state = {
name: "Micheal",
age: 32
}
// Let's assume the user is in a wizard, and we don't know whether
// his changes should end up in the base state ultimately or not...
let fork = state
// all the changes the user made in the wizard
let changes = []
// the inverse of all the changes made in the wizard
let inverseChanges = []
fork = produce(
fork,
draft => {
draft.age = 33
},
// The third argument to produce is a callback to which the patches will be fed
(patches, inversePatches) => {
changes.push(...patches)
inverseChanges.push(...inversePatches)
}
)
// In the meantime, our original state is replaced, as, for example,
// some changes were received from the server
state = produce(state, draft => {
draft.name = "Michel"
})
// When the wizard finishes (successfully) we can replay the changes that were in the fork onto the *new* state!
state = applyPatches(state, changes)
// state now contains the changes from both code paths!
expect(state).toEqual({
name: "Michel", // changed by the server
age: 33 // changed by the wizard
})
// Finally, even after finishing the wizard, the user might change his mind and undo his changes...
state = applyPatches(state, inverseChanges)
expect(state).toEqual({
name: "Michel", // Not reverted
age: 32 // Reverted
})The generated patches are similar (but not the same) to the RFC-6902 JSON patch standard, except that the path property is an array, rather than a string. This makes processing patches easier. If you want to normalize to the official specification, use patch.path = "/" + patch.path.join("/") so the result is a JSON Pointer with a leading / (and escape ~ / / in path tokens per RFC-6901 when needed). Anyway, this is what a bunch of patches and their inverse could look like:
[
{
"op": "replace",
"path": ["profile"],
"value": {"name": "Veria", "age": 5}
},
{"op": "remove", "path": ["tags", 3]}
][
{"op": "replace", "path": ["profile"], "value": {"name": "Noa", "age": 6}},
{"op": "add", "path": ["tags", 3], "value": "kiddo"}
]โ Note: 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. It depends often on the use case what is considered 'optimal', and generating the optimal set of patches is potentially computationally very expensive. So in cases you might want to post process the generated patches, or compress them as explained below.
`produceWithPatches`
egghead.io lesson 19: Using inverse patches to build undo functionality
<a
className="egghead-link"
href="https://egghead.io/lessons/react-use-immer-inversepatches-to-build-undo-functionality"
>
Hosted on egghead.io
</a>
egghead.io lesson 20: Use patches to build redo functionality
<a
className="egghead-link"
href="https://egghead.io/lessons/react-use-immer-patches-to-build-redo-functionality"
>
Hosted on egghead.io
</a>Instead of setting up a patch listener, an easier way to obtain the patches is to use produceWithPatches, which has the same signature as produce, except that it doesn't return just the next state, but a tuple consisting of [nextState, patches, inversePatches]. Like produce, produceWithPatches supports currying as well.
import {produceWithPatches} from "immer"
const [nextState, patches, inversePatches] = produceWithPatches(
{
age: 33
},
draft => {
draft.age++
}
)Which produces:
;[
{
age: 34
},
[
{
op: "replace",
path: ["age"],
value: 34
}
],
[
{
op: "replace",
path: ["age"],
value: 33
}
]
]For a more in-depth study, see Distributing patches and rebasing actions using Immer
Tip: Check this trick to compress patches produced over time.
Performance (`website/docs/performance.mdx`)
id: performance title: Immer performance
egghead.io lesson 5: Leveraging Immer's structural sharing in React
<a
className="egghead-link"
href="https://egghead.io/lessons/react-profile-react-rendering-and-optimize-with-memo-to-leverage-structural-sharing"
>
Hosted on egghead.io
</a>
egghead.io lesson 7: Immer will try to re-cycle data if there was no
semantic change
<a
className="egghead-link"
href="https://egghead.io/lessons/javascript-produces-immutable-data-and-avoid-unnecessary-creation-of-new-data-trees-with-immer"
>
Hosted on egghead.io
</a>Here is a simple benchmark on the performance of Immer. This test takes 50,000 todo items and updates 5,000 of them. Freeze indicates that the state tree has been frozen after producing it. This is a development best practice, as it prevents developers from accidentally modifying the state tree.
Something that isn't reflected in the numbers above, but in reality, Immer is sometimes significantly faster than a hand written reducer. The reason for that is that Immer will detect "no-op" state changes, and return the original state if nothing actually changed, which can avoid a lot of re-renderings for example. Cases are known where simply applying immer solved critical performance issues.
These tests were executed on Node 10.16.3. Use yarn test:perf to reproduce them locally.
Most important observation:
- Immer with proxies is roughly speaking twice to three times slower as a handwritten reducer (the above test case is worst case, see
yarn test:perffor more tests). This is in practice negligible. - Immer is roughly as fast as ImmutableJS. However, the immutableJS + toJS makes clear the cost that often needs to be paid later; converting the immutableJS objects back to plain objects, to be able to pass them to components, over the network etc... (And there is also the upfront cost of converting data received from e.g. the server to immutable JS)
- Generating patches doesn't significantly slow down immer
- The ES5 fallback implementation is roughly twice as slow as the proxy implementation, in some cases worse.
Performance tips
Enable the Array Methods Plugin
For applications with significant array iteration within producers, enable the Array Methods Plugin:
import {enableArrayMethods} from "immer"
enableArrayMethods()This plugin optimizes array operations like filter, find, some, every, and slice by avoiding proxy creation for every element during iteration. Without the plugin, iterating a 1000-element array creates 1000+ proxies. With the plugin, callbacks receive base values, and proxies are only created for elements you actually mutate.
Use loose iteration for better performance
By default, Immer uses loose iteration which only processes enumerable string properties. This is faster than strict iteration which includes symbols and non-enumerable properties. For most use cases, the default is optimal:
import {setUseStrictIteration} from "immer"
// Default: false (loose iteration for better performance)
setUseStrictIteration(false)Only enable strict iteration if you specifically need to track symbol or non-enumerable properties.
Pre-freeze data
When adding a large data set to the state tree in an Immer producer (for example data received from a JSON endpoint), it is worth to call freeze(json) on the root of the data that is being added first. To shallowly freeze it. This will allow Immer to add the new data to the tree faster, as it will avoid the need to recursively scan and freeze the new data.
You can always opt-out
Realize that immer is opt-in everywhere, so it is perfectly fine to manually write super performance critical reducers, and use immer for all the normal ones. Even from within a producer you opt-out from Immer for certain parts of your logic by using utilies original or current and perform some of your operations on plain JavaScript objects.
For expensive search operations, read from the original state, not the draft
Immer will convert anything you read in a draft recursively into a draft as well. If you have expensive side effect free operations on a draft that involves a lot of reading, for example finding an index using find(Index) in a very large array, you can speed this up by first doing the search, and only call the produce function once you know the index. Thereby preventing Immer to turn everything that was searched for in a draft. Or, alternatively, perform the search on the original value of a draft, by using original(someDraft), which boils to the same thing.
Pull produce as far up as possible
Always try to pull produce 'up', for example for (let x of y) produce(base, d => d.push(x)) is exponentially slower than produce(base, d => { for (let x of y) d.push(x)})
Pitfalls (`website/docs/pitfalls.md`)
id: pitfalls title: 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 of the outer produce; those changes will only be visible in the output that the inner produce returns. 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 of 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.
Array Methods Plugin: Callbacks receive base values
When using the Array Methods Plugin (enableArrayMethods()), callbacks for overridden methods like filter, find, some, every, and slice receive base values (not drafts). This is the core performance optimization - it avoids creating proxies for every element during iteration.
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
})This only applies to methods intercepted by the plugin. Methods like map, forEach, reduce are NOT overridden and work normally - their callbacks receive drafts.
Produce (`website/docs/produce.mdx`)
id: produce title: Using produce
egghead.io lesson 3: Simplifying deep updates with _produce_
<a
className="egghead-link"
href="https://egghead.io/lessons/javascript-simplify-deep-state-updates-using-immer-produce"
>
Hosted on egghead.io
</a>The Immer package exposes a produce function that does all the work.
produce(baseState, recipe: (draftState) => void): nextState
produce takes a base state, and a recipe that can be used to perform all the desired mutations on the draft that is passed in. The interesting thing about Immer is that the baseState will be untouched, but the nextState will reflect all changes made to draftState.
Inside the recipe, all standard JavaScript APIs can be used on the draft object, including field assignments, delete operations, and mutating array, Map and Set operations like push, pop, splice, set, sort, remove, etc.
Any of those mutations don't have to happen at the root, but it is allowed to modify anything anywhere deep inside the draft: draft.todos[0].tags["urgent"].author.age = 56
Note that the recipe function itself normally doesn't return anything. However, it is possible to return in case you want to replace the draft object in its entirety with another object, for more details see returning new data.
Example
import {produce} from "immer"
const baseState = [
{
title: "Learn TypeScript",
done: true
},
{
title: "Try Immer",
done: false
}
]
const nextState = produce(baseState, draftState => {
draftState.push({title: "Tweet about it"})
draftState[1].done = true
})// the new item is only added to the next state,
// base state is unmodified
expect(baseState.length).toBe(2)
expect(nextState.length).toBe(3)
// same for the changed 'done' prop
expect(baseState[1].done).toBe(false)
expect(nextState[1].done).toBe(true)
// unchanged data is structurally shared
expect(nextState[0]).toBe(baseState[0])
// ...but changed data isn't.
expect(nextState[1]).not.toBe(baseState[1])For advanced TypeScript typing scenarios, see Using TypeScript or Flow.
Terminology
(base)state, the immutable state passed toproducerecipe: the second argument ofproduce, that captures how the base state should be "mutated".draft: the first argument of anyrecipe, which is a proxy to the original base state that can be safely mutated.producer. A function that usesproduceand is generally of the form(baseState, ...arguments) => resultState
Note that it isn't strictly necessary to name the first argument of the recipe draft. You can name it anything you want, for example users. Using draft as a name is just a convention to signal: "mutation is OK here".
Resources (`website/docs/resources.md`)
id: resources title: External resources
- Blog: The Rise of Immer in React
- Blog: by Workday Prism on why they picked Immer to manage immutable state The Search for a Strongly-Typed, Immutable State
- Blog: Immutability in React and Redux: The Complete Guide
- Video tutorial: Using Immer with React.setState
- Talk + slides on Immer at React Finland 2018 by Michel Weststrate
- ForwardJS 2019: Immutability is Changing - From Immutable.js to Immer by shawn swyx wang
- Talk: Immer, Immutability and the Wonderful World of Proxies + slides, JSNation 2019, Michel Weststrate
- Blog: Distributing state changes using snapshots, patches and actions
- Blog: Implementing Undo-Redo Functionality in Redux, Sep 2019
- Blog: Synchronized immutable state with time travel, Apr 2022, by Oleg Isonen
Return (`website/docs/return.mdx`)
id: return title: Returning new data from producers
egghead.io lesson 9: Returning completely new state
<a
className="egghead-link"
href="https://egghead.io/lessons/react-return-completely-new-state-from-an-immer-producer"
>
Hosted on egghead.io
</a>It is not needed to return anything from a producer, as Immer will return the (finalized) version of the draft anyway. However, it is allowed to just return draft.
It is also allowed to return arbitrarily other data from the producer function. But only if you didn't modify the draft. This can be useful to produce an entirely new state. Some examples:
const userReducer = produce((draft, action) => {
switch (action.type) {
case "renameUser":
// OK: we modify the current state
draft.users[action.payload.id].name = action.payload.name
return draft // same as just 'return'
case "loadUsers":
// OK: we return an entirely new state
return action.payload
case "adduser-1":
// NOT OK: This doesn't do change the draft nor return a new state!
// It doesn't modify the draft (it just redeclares it)
// In fact, this just doesn't do anything at all
draft = {users: [...draft.users, action.payload]}
return
case "adduser-2":
// NOT OK: modifying draft *and* returning a new state
draft.userCount += 1
return {users: [...draft.users, action.payload]}
case "adduser-3":
// OK: returning a new state. But, unnecessary complex and expensive
return {
userCount: draft.userCount + 1,
users: [...draft.users, action.payload]
}
case "adduser-4":
// OK: the immer way
draft.userCount += 1
draft.users.push(action.payload)
return
}
})Note: It is not possible to return undefined this way, as it is indistinguishable from not updating the draft! Read on...
Producing `undefined` using `nothing`
So, in general, one can replace the current state by just returning a new value from the producer, rather than modifying the draft. There is a subtle edge case however: if you try to write a producer that wants to replace the current state with undefined:
produce({}, draft => {
// don't do anything
})Versus:
produce({}, draft => {
// Try to return undefined from the producer
return undefined
})The problem is that in JavaScript a function that doesn't return anything also returns undefined! So immer cannot differentiate between those different cases. So, by default, Immer will assume that any producer that returns undefined just tried to modify the draft.
However, to make it clear to Immer that you intentionally want to produce the value undefined, you can return the built-in token nothing:
import {produce, nothing} from "immer"
const state = {
hello: "world"
}
produce(state, draft => {})
produce(state, draft => undefined)
// Both return the original state: { hello: "world"}
produce(state, draft => nothing)
// Produces a new state, 'undefined'N.B. Note that this problem is specific for the undefined value, any other value, including null, doesn't suffer from this issue.
Tip: to be able to return nothing from a recipe when using TypeScript, the state's type must accept undefined as value.
Inline shortcuts using `void`
egghead.io lesson 10: Avoid accidental returns by using _void_
<a
className="egghead-link"
href="https://egghead.io/lessons/react-avoid-accidental-returns-of-new-state-by-using-the-void-keyword"
>
Hosted on egghead.io
</a>Draft mutations in Immer usually warrant a code block, since a return denotes an overwrite. Sometimes that can stretch code a little more than you might be comfortable with.
In such cases, you can use javascripts void operator, which evaluates expressions and returns undefined.
// Single mutation
produce(draft => void (draft.user.age += 1))
// Multiple mutations
produce(draft => void ((draft.user.age += 1), (draft.user.height = 186)))Code style is highly personal, but for code bases that are to be understood by many, we recommend to stick to the classic draft => { draft.user.age += 1} to avoid cognitive overhead.
Support (`website/docs/support.md`)
id: support title: Supporting immer
Immer currently has 350.000 dependents on GitHub, and has almost 10.000.000 downloads per month. However, only the first 2 days of development has been sponsored (by Mendix), and all the development and maintenance after that has been a labor of love.
If you are enjoying Immer, and you are grateful for the package, or want to ensure its longevity, consider sponsoring the open collective on https://opencollective.com/immer or make a one time donation using PayPal.
Typescript (`website/docs/typescript.mdx`)
id: typescript title: Using TypeScript or Flow sidebar_label: TypeScript / Flow
egghead.io lesson 12: Immer + TypeScript
<a
className="egghead-link"
href="https://egghead.io/lessons/react-type-immutable-immer-data-with-typescript"
>
Hosted on egghead.io
</a>The Immer package ships with type definitions inside the package, which should be picked up by TypeScript and Flow out of the box and without further configuration.
The TypeScript typings automatically remove readonly modifiers from your draft types and return a value that matches your original type. See this practical example:
import {produce} from "immer"
interface State {
readonly x: number
}
// `x` cannot be modified here
const state: State = {
x: 0
}
const newState = produce(state, draft => {
// `x` can be modified here
draft.x++
})
// `newState.x` cannot be modified hereThis ensures that the only place you can modify your state is in your produce callbacks. It even works recursively and with ReadonlyArray.
Best practices
- Always define your states as
readonlyas much as possible. This best reflects the mental model and reality, since Immer will freeze all its returned values. - You can use the utility type
Immutableto recursively make an entire type tree read-only, e.g.:type ReadonlyState = Immutable<State>. - Immer won't automatically wrap all returned types in
Immutableif the original type of the input state wasn't immutable. This is to make sure it doesn't break code bases that don't use immutable types.
Typing reusable recipes
If you want to pass a recipe callback around, you can type it using Draft and allow either void or a replacement state as the return value.
import {produce} from "immer"
import type {Draft} from "immer"
type Recipe<S> = (draft: Draft<S>) => void | S
function updateState<S>(
setState: (updater: (prev: S) => S) => void,
recipe: Recipe<S>
) {
setState(prev => produce(prev, recipe))
}React example
A common usage in React is to wrap setState with produce so callers can provide a typed recipe.
import {produce} from "immer"
import type {Draft} from "immer"
import {useCallback, useState} from "react"
type State = {
foo: string
bar: string
}
const initialState: State = {
foo: "",
bar: ""
}
type Recipe = (draft: Draft<State>) => void | State
export function Example() {
const [state, setState] = useState<State>(initialState)
const updateState = useCallback((recipe: Recipe) => {
setState(prev => produce(prev, recipe))
}, [])
return (
<button
onClick={() => {
updateState(draft => {
draft.foo = "Muffin Man" // this recipe is typed
})
}}
type="button"
>
Hello {state.foo}
</button>
)
}Tips for curried producers
We try to inference as much as possible. So if a curried producer is created and directly passed to another function, we can infer the type from there. This works well with for example React:
import {Immutable, produce} from "immer"
type Todo = Immutable<{
title: string
done: boolean
}>
// later...
const [todo, setTodo] = useState<Todo>({
title: "test",
done: true
})
// later...
setTodo(
produce(draft => {
// draft will be strongly typed and mutable!
draft.done = !draft.done
})
)When a curried producer isn't passed directly somewhere else, Immer can infer the state type from the draft argument. For example when doing the following:
// See below for a better solution!
const toggler = produce((draft: Draft<Todo>) => {
draft.done = !draft.done
})
// typeof toggler = (state: Immutable<Todo>) => Writable<Todo>Note that we did wrap the Todo type of the draft argument with Draft, because Todo is a readonly type. For non-readonly types this isn't needed.
For the returned curried function, toggler, We will narrow the input type to Immutable<Todo>, so that even though Todo is a mutable type, we will still accept an immutable todo as input argument to toggler.
In contrast, Immer will widen the output type of the curried function to Writable<Todo>, to make sure it's output state is also assignable to variables that are not explicitly typed to be immutable.
This type narrowing / widening behavior might be unwelcome, maybe even for the simple reason that it results in quite noisy types. So we recommend to specify the generic state type for curried producers instead, in cases where it cannot be inferred directly, like toggler above. By doing so the automatic output widening / input narrowing will be skipped. However, the draft argument itself will still be inferred to be a writable Draft<Todo>:
const toggler = produce<Todo>(draft => {
draft.done = !draft.done
})
// typeof toggler = (state: Todo) => TodoHowever, in case the curried producer is defined with an initial state, Immer can infer the state type from the initial state, so in that case the generic doesn't need to be specified either:
const state0: Todo = {
title: "test",
done: false
}
// No type annotations needed, since we can infer from state0.
const toggler = produce(draft => {
draft.done = !draft.done
}, state0)
// typeof toggler = (state: Todo) => TodoIn case the toggler has no initial state, and it has curried arguments, and you set the state generic explicitly, then type of any additional arguments should be defined explicitly as a tuple type as well:
const toggler = produce<Todo, [boolean]>((draft, newState) => {
draft.done = newState
})
// typeof toggler = (state: Todo, newState: boolean) => TodoCast utilities
The types inside and outside a produce can be conceptually the same, but from a practical perspective different. For example, the State in the examples above should be considered immutable outside produce, but mutable inside produce.
Sometimes this leads to practical conflicts. Take the following example:
type Todo = {readonly done: boolean}
type State = {
readonly finishedTodos: readonly Todo[]
readonly unfinishedTodos: readonly Todo[]
}
function markAllFinished(state: State) {
produce(state, draft => {
draft.finishedTodos = state.unfinishedTodos
})
}This will generate the error:
The type 'readonly Todo[]' is 'readonly' and cannot be assigned to the mutable type '{ done: boolean; }[]'The reason for this error is that we assign our read only, immutable array to our draft, which expects a mutable type, with methods like .push etc etc. As far as TS is concerned, those are not exposed from our original State. To hint TypeScript that we want to upcast the collection here to a mutable array for draft purposes, we can use the utility castDraft:
draft.finishedTodos = castDraft(state.unfinishedTodos) will make the error disappear.
There is also the utility castImmutable, in case you ever need to achieve the opposite. Note that these utilities are for all practical purposes no-ops, they will just return their original value.
Tip: You can combine castImmutable with produce to type the return type of produce as something immutable, even when the original state was mutable:
// a mutable data structure
const baseState = {
todos: [{
done: false
}]
}
const nextState = castImmutable(produce(baseState, _draft => {}))
// inferred type of nextState is now:
{
readonly todos: ReadonlyArray<{
readonly done: boolean
}>
})Compatibility
Note: Immer v5.3+ supports TypeScript v3.7+ only.
Note: Immer v3.0+ supports TypeScript v3.4+ only.
Note: Immer v1.9+ supports TypeScript v3.1+ only.
Note: Flow support might be removed in future versions and we recommend TypeScript
Update Patterns (`website/docs/update-patterns.md`)
id: update-patterns title: Update patterns
Working with immutable data, before Immer, used to mean learning all the immutable update patterns.
To help 'unlearning' those patterns here is an overview how you can leverage the built-in JavaScript APIs to update objects and collections:
Object mutations
import {produce} from "immer"
const todosObj = {
id1: {done: false, body: "Take out the trash"},
id2: {done: false, body: "Check Email"}
}
// add
const addedTodosObj = produce(todosObj, draft => {
draft["id3"] = {done: false, body: "Buy bananas"}
})
// delete single property
const deletedTodosObj = produce(todosObj, draft => {
delete draft["id1"]
})
// update
const updatedTodosObj = produce(todosObj, draft => {
draft["id1"].done = true
})
// replace & update in bulk
const updatedTodosObj = produce(todosObj, draft => {
Object.assign(draft, {
id1: {done: true, body: "Take out the trash"},
id2: {done: true, body: "Check Email"},
id3: {done: true, body: "Feed my cat"}
})
// reset/clear/empty
const emptyTodo = produce(todosObj, () => {
return {};
})Any time a nested draft field gets a new reference or value, produce() will finish applying the immutable update and return a new reference. If you tried to mutate, but the values remained the same, Immer will bail out and return the existing reference from produce()
Array mutations
/* Detailed source-code truncated for AI context efficiency. */Nested data structures
import {produce} from "immer"
// example complex data structure
const store = {
users: new Map([
[
"17",
{
name: "Michel",
todos: [
{
title: "Get coffee",
done: false
}
]
}
]
])
}
// updating something deeply in-an-object-in-an-array-in-a-map-in-an-object:
const nextStore = produce(store, draft => {
draft.users.get("17").todos[0].done = true
})
// filtering out all unfinished todo's
const nextStore = produce(store, draft => {
const user = draft.users.get("17")
// when filtering, creating a fresh collection is simpler than
// removing irrelevant items
user.todos = user.todos.filter(todo => !todo.done)
})Note that many array operations can be used to insert multiple items at once by passing multiple arguments or using the spread operation: todos.unshift(...items).
Note that when working with arrays that contain objects that are typically identified by some id, we recommend to use Map or index based objects (as shown above) instead of performing frequent find operations, lookup tables perform much better in general.
--- METRICS ---
- Files Extracted: 26
- Estimated Token Budget: ~22907 tokens
- Canonical Reference: https://codewiki.google/github.com/immerjs/immer