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
Did Immer make a difference to your project? Consider buying me a coffee!
npm install immer
yarn add immer
immer
<script
src="https://unpkg.com/immer/dist/immer.umd.js"></script>
<script
src="https://cdn.jsdelivr.net/npm/immer/dist/immer.umd.js"></script>
Immer (German for: always) is a tiny package that allows you to work with immutable state in a more convenient way. It is based on the copy-on-write mechanism.
The basic idea is that you will apply all your changes to a temporary draftState, 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; he 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).
A mindful reader might notice that this is quite similar to
withMutations
of ImmutableJS. It is indeed, but generalized and
applied to plain, native JavaScript data structures (arrays and objects)
without further needing any library.
The Immer package exposes a default function that does all the work.
produce(currentState, producer: (draftState) => void): nextState
There is also a curried overload that is explained below.
import produce from "immer"
const baseState = [
{
todo: "Learn typescript",
done: true
},
{
todo: "Try immer",
done: false
}
]
const nextState = produce(baseState, draftState => {
draftState.push({todo: "Tweet about it"})
draftState[1].done = true
})
The interesting thing about Immer is that the baseState
will be
untouched, but the nextState
will reflect all changes made to
draftState
.
// 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])
// changed data not (dûh)
expect(nextState[1]).not.toBe(baseState[1])
Read further to see all these benefits explained.
Here is a simple example of the difference that Immer could make in practice.
// Redux reducer
// Shortened, based on: https://github.com/reactjs/redux/blob/master/examples/shopping-cart/src/reducers/products.js
const byId = (state, action) => {
switch (action.type) {
case RECEIVE_PRODUCTS:
return {
...state,
...action.products.reduce((obj, product) => {
obj[product.id] = product
return obj
}, {})
}
default:
return state
}
}
After using Immer, that simply becomes:
import produce from "immer"
const byId = (state, action) =>
produce(state, draft => {
switch (action.type) {
case RECEIVE_PRODUCTS:
action.products.forEach(product => {
draft[product.id] = product
})
}
})
Notice that it is not needed to handle the default case, a producer that doesn't do anything will simply return the original state.
Creating Redux reducer is just a sample application of the Immer package. Immer is not just designed to simplify Redux reducers. It can be used in any context where you have an immutable data tree that you want to clone and modify (with structural sharing).
Note: it might be tempting after using producers for a while, to just place
produce
in your root reducer and then pass the draft to each
reducer and work directly over such draft. Don't do that. It kills the point
of Redux where each reducer is testable as pure reducer. Immer is best used
when applying it to small individual pieces of logic.
Deep updates in the state of React components can be greatly simplified as well by using immer. Take for example the following onClick handlers (Try in codesandbox):
/**
* Classic React.setState with a deep merge
*/
onBirthDayClick1 = () => {
this.setState(prevState => ({
user: {
...prevState.user,
age: prevState.user.age + 1
}
}))
}
/**
* ...But, since setState accepts functions,
* we can just create a curried producer and further simplify!
*/
onBirthDayClick2 = () => {
this.setState(
produce(draft => {
draft.user.age += 1
})
)
}
Passing a function as the first argument to produce
is intended
to be used for currying. This means that you get a pre-bound producer that
only needs a state to produce the value from. The producer function gets
passed in the draft and any further arguments that were passed to the curried
function.
For example:
// mapper will be of signature (state, index) => state
const mapper = produce((draft, index) => {
draft.index = index
})
// example usage
console.dir([{}, {}, {}].map(mapper))
//[{index: 0}, {index: 1}, {index: 2}])
This mechanism can also nicely be leveraged to further simplify our example reducer:
import produce from "immer"
const byId = produce((draft, action) => {
switch (action.type) {
case RECEIVE_PRODUCTS:
action.products.forEach(product => {
draft[product.id] = product
})
return
}
})
Note that state
is now factored out (the created reducer will
accept a state, and invoke the bound producer with it).
If you want to initialize an uninitialized state using this construction, you
can do so by passing the initial state as second argument to
produce
:
import produce from "immer"
const byId = produce(
(draft, action) => {
switch (action.type) {
case RECEIVE_PRODUCTS:
action.products.forEach(product => {
draft[product.id] = product
})
return
}
},
{
1: {id: 1, name: "product-1"}
}
)
A random fun example just for inspiration: a neat trick is to turn
Object.assign
into a producer to create a "spread"
function that is smarter than the normal spread operator, as it doesn't
produce a new state if the result doesn't actually change (details & explanation). Quick example:
import produce from "immer"
const spread = produce(Object.assign)
const base = {x: 1, y: 1}
console.log({...base, y: 1} === base) // false
console.log(spread(base, {y: 1}) === base) // true! base is recycled as no actual new value was produced
console.log(spread(base, {y: 2}) === base) // false, produced a new object as it should
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 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"
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, patch.path = patch.path.join("/")
should
do the trick. 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"}
]
produceWithPatches
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.
It is allowed to return Promise objects from recipes. Or, in other words, to
use async / await
. This can be pretty useful for long running
processes, that only produce the new object once the promise chain resolves.
Note that produce
itself (even in the curried form) will return a
promise if the producer is async. Example:
import produce from "immer"
const user = {
name: "michel",
todos: []
}
const loadedUser = await produce(user, async function(draft) {
draft.todos = await (await window.fetch("http://host/" + draft.name)).json()
})
Warning: please note that the draft shouldn't be 'leaked' from the async process and stored else where. The draft will still be revoked as soon as the async process completes.
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. We could for example rewrite our above example as:
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: 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.
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...
undefined
using nothing
So, in general, one can replace the current state by just
return
ing 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.
void
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.
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} 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!
import {isDraft} 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) // => false
Immer automatically freezes any state trees that are modified using
produce
. This protects against accidental modifications of the
state tree outside of a producer. This comes with a performance impact, so it
is recommended to disable this option in production. It is by default enabled.
By default, it is turned on during local development and turned off in
production. Use setAutoFreeze(true / false)
to explicitly turn
this feature on or off.
⚠️ 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! ⚠️
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 (< v0.59)
on Android. In such cases, Immer will fallback to an ES5 compatible
implementation which works identical, but is a bit slower.
produce
is exposed as the default export, but optionally it can
be used as name import as well, as this benefits some older project setups. So
the following imports are all correct, where the first is recommended:
import produce from "immer"
import {produce} from "immer"
const {produce} = require("immer")
const produce = require("immer").produce
const produce = require("immer").default
import unleashTheMagic from "immer"
import {produce as unleashTheMagic} from "immer"
Plain objects and arrays 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 3
For arrays, only numeric properties and the length
property can
be mutated. Custom properties are not preserved on arrays.
When working with Date
objects, you should always create a new
Date
instance instead of mutating an existing
Date
object.
Built-in classes like Map
and Set
are not supported.
As a workaround, you should clone them before mutating them in a producer:
const state = {
set: new Set(),
map: new Map()
}
const nextState = produce(state, draft => {
// Don't use any Set methods, as that mutates the instance!
draft.set.add("foo") // ❌
// 1. Instead, clone the set (just once)
const newSet = new Set(draft.set) // ✅
// 2. Mutate the clone (just in this producer)
newSet.add("foo")
// 3. Update the draft with the new set
draft.set = newSet
// Similarly, don't use any Map methods.
draft.map.set("foo", "bar") // ❌
// 1. Instead, clone the map (just once)
const newMap = new Map(draft.map) // ✅
// 2. Mutate it
newMap.set("foo", "bar")
// 3. Update the draft
draft.map = newMap
})
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 here
This ensures that the only place you can modify your state is in your produce
callbacks. It even works recursively and with ReadonlyArray
s!
For curried reducers, the type is inferred from the first argument of recipe
function, so make sure to type it. The Draft
utility type can be
used if the state argument type is immutable:
import produce, {Draft} from "immer"
interface State {
readonly x: number
}
// `x` cannot be modified here
const state: State = {
x: 0
}
const increment = produce((draft: Draft<State>, inc: number) => {
// `x` can be modified here
draft.x += inc
})
const newState = increment(state, 2)
// `newState.x` cannot be modified here
Note: Immer v1.9+ supports TypeScript v3.1+ only.
Note: Immer v3.0+ supports TypeScript v3.4+ only.
draft = myCoolNewState
. Instead,
either modify the draft
or return a new state. See
Returning data from producers.
currentState
rather than the draftState
. Also,
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. Also note that original
can be used to get the original
state of an object, which is cheaper to read.
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)})
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.
immer
state. Like redux-immutable
but
for immer
Read the (second part of the) introduction blog.
For those who have to go back to thinking in object updates :-)
import produce from "immer"
// object mutations
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
const deletedTodosObj = produce(todosObj, draft => {
delete draft["id1"]
})
// update
const updatedTodosObj = produce(todosObj, draft => {
draft["id1"].done = true
})
// array mutations
const todosArray = [
{id: "id1", done: false, body: "Take out the trash"},
{id: "id2", done: false, body: "Check Email"}
]
// add
const addedTodosArray = produce(todosArray, draft => {
draft.push({id: "id3", done: false, body: "Buy bananas"})
})
// delete
const deletedTodosArray = produce(todosArray, draft => {
draft.splice(draft.findIndex(todo => todo.id === "id1"), 1)
// or (slower):
// return draft.filter(todo => todo.id !== "id1")
})
// update
const updatedTodosArray = produce(todosArray, draft => {
draft[draft.findIndex(todo => todo.id === "id1")].done = true
})
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.
These tests were executed on Node 9.3.0. Use yarn test:perf
to
reproduce them locally.
Most important observation:
yarn test:perf
for more tests). This is in practice negligible.
Immer 2.* -> 3.0
In your producers, make sure you're not treating this
as the
draft. (see here: https://github.com/immerjs/immer/issues/308)
Upgrade to typescript@^3.4
if you're a TypeScript user.
Immer 1.* -> 2.0
Make sure you don't return any promises as state, because
produce
will actually invoke the promise and wait until it
settles.
Immer 2.1 -> 2.2
When using TypeScript, for curried reducers that are typed in the form
produce<Type>((arg) => { })
, rewrite this to
produce((arg: Type) => { })
or
produce((arg: Draft<Type>) => { })
for correct
inference.
(for those who skimmed the above instead of actually reading)
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
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: Is it fast?
A: Yes
Q: Idea! Can Immer freeze the state for me?
A: Yes
Special thanks to @Mendix, which supports its employees to experiment completely freely two full days a month, which formed the kick-start for this project.
A significant part of my OSS work is unpaid. So donations are greatly appreciated :)