Immer

5 min read
~940 words
17/07/26
17/07/26

Immer is a very popular JavaScript library, often used with React to make developer's lives easier. Apparently it's so good it's won a couple awards!

But what is immer, and more importantly, why do people use it?

The problem

React, and some other frameworks assume that state is immutable, that once it has a piece of data it will stay the same for the rest of eternity. Unfortunately, a pesky group of people called "users" expects that UIs update when you click buttons and submit forms and yell at LLMs to stop being so stupid and do what I told you to.

So for the blessing of interactivity, the React gods require that when we update state, the new value doesn't equal the old value. More specifically, React checks that Object.is(old, new) returns false. This works out alright for simple values like numbers, strings and booleans. However, this gets annoying when we use arrays and objects, since Object.is just checks if the values are the same by reference, instead of by value.

How it looks

If you want to add an item to the end of an array, the obvious thing to do in JavaScript is push to it, then tell React it's changed:

javascript
snacks.push('Grapes')
setSnacks(snacks)

When you push to an array, it's still the same array, we're just mutating it and the updated array still has the same reference as the old one! (like how you're still the same person if you eat some grapes) Now when React compares the new and old state values, it ends up running something like Object.is(snacks, snacks). This will return true, and your UI won't update.

For React to update you're UI, you might do something like this:

javascript
snacks.push('Oreos')
const updatedSnacks = [...snacks] // or snacks.slice()
snacks.setSnacks(updatedSnacks)

or if you want to add the item, and make a copy at the same time:

javascript
const updatedSnacks = [...snacks, 'Jelly Beans']
snacks.setSnacks(updatedSnacks)

This isn't too bad since the values are so simple, but...

It gets worse

If you want to update a more complex object, the naïve approach would look like this:

javascript
const transfer = {
  from: 'alice',
  to: 'bob',
  snack: snackToTransfer,
  count: 1,
}
 
const fromPerson = data.people.find(person => person.id === transfer.from)
const fromSnack = fromPerson.snacks.find(snack => snack.id === transfer.snack)
fromSnack.count -= transfer.count
 
const toPerson = data.people.find(person => person.id === transfer.to)
const toSnack = toPerson.snacks.find(snack => snack.id === transfer.snack)
toSnack.count += transfer.count
 
data.transfers.push(transfer)
 
setData(data)

But an update without mutation would be this monstrosity (after a light refactor):

javascript
const transfer = {
  from: 'alice',
  to: 'bob',
  snack: snackToTransfer,
  count: 1,
}
 
function updatePersonSnacks(person, transfer, direction) {
  const directionMultiplier = direction === 'from' ? -1 : 1
 
  return {
    ...person,
    snacks: person.snacks.map(snack => {
      if (snack.id !== transfer.snack) {
        return snack
      }
 
      return {
        ...snack,
        count: snack.count + directionMultiplier * transfer.count,
      }
    }),
  }
}
 
const updatedData = {
  ...data,
  people: data.people.map(person => {
    if (person.id === transfer.from) {
      return updatePersonSnacks(person, transfer, 'from')
    } else if (person.id === transfer.to) {
      return updatePersonSnacks(person, transfer, 'to')
    } else {
      return person
    }
  }),
 
  transfers: [...data.transfers, transfer],
}
 
setData(updatedData)

This is clearly more verbose, complex and yuck. But it's also the kind of complexity Immer is designed to solve.

How immer helps

Immer provides a (in my opinion) confusingly-named produce function that lets you write code almost identical to the naïve approaches above. Re-working the naive version of the complex example above to work with Immer, all we really need to do is add a few lines, and rename a variable. It's so easy you might hand-writing the code!!

javascript
const transfer = {
  from: 'alice',
  to: 'bob',
  snack: snackToTransfer,
  count: 1,
}
 
const updatedData = produce(data, draft => {
  const fromPerson = draft.people.find(person => person.id === transfer.from)
  const fromSnack = fromPerson.snacks.find(snack => snack.id === transfer.snack)
  fromSnack.count -= transfer.count
 
  const toPerson = draft.people.find(person => person.id === transfer.to)
  const toSnack = toPerson.snacks.find(snack => snack.id === transfer.snack)
  toSnack.count += transfer.count
 
  draft.transfers.push(transfer)
})
 
setData(updatedData)

Immer takes our naïve mutations, and produces a copy of our data with the required changes to get React to update our UIs. It's made possible by the draft object (you can call it whatever you want), which we mutate rather than the actual data. This allows Immer to keep track of the changes we make, powered by the dark magic of proxies.

If you're extra lazy there's a use-immer library that ships a useImmer hook, the love-child of React's setState and Immer's produce. Using useImmer you can write a whole 1 fewer line.

Should you use it?

Immer makes update state significantly simpler and easier to read. It's still useful if you're not using React's state management system. Redux Toolkit, builds on top of the definitely-extremely-verbose Redux using libraries like Immer to make developers' lives easier.

There are a series of pitfalls to keep in mind and as usual, Immer's also not the only library to do this kind of thing. I found another one called mutative that claims to be up to 10x faster and so could be a better fit for your use case.

Immer's approach also isn't really needed if your state isn't overly complex, and can instead add unnecessary overhead, and complex state might be a sign you need to refactor. It'll become even less necessary as we read code less, and delegate more of that responsibility to LLMs.

You and your local LLM should think about whether Immer is the right tool for your project.


why's it always "what is immer" and "why is immer" but never "how is immer" 😭😭😭