Skip to main content

Returning new data from producers

egghead.io lesson 9: Returning completely new state

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_

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.