Immer: immutable state in JavaScript by writing mutable code
Create the next immutable state by mutating the current one
At a glance
- What is it?
- Immer lets you produce the next immutable state tree by mutating a draft of the current one. Here is the mechanism, the install path, and where the approach stops paying off.
- Who is it for?
- Adopt Immer when your reducers or store updates already read like a list of assignments and you want those assignments to stay in place while the surrounding tree stays immutable. Skip it when your updates are already a single shallow spread, or when you cannot accept a Proxy on the hot path and a production build that relies on freezing in development.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 18 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 28, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem Immer solves: nested updates without spread pyramids
Updating a value three levels deep in an immutable tree normally means rebuilding every object on the path to it. With plain JavaScript that becomes a spread at each level, and the code that performs the update looks nothing like the shape of the data. Immer's pitch is that you write the update as if the data were mutable, then let the library decide which parts of the tree actually changed.
The audience is narrow but large. It is anyone maintaining reducers, store slices, or any function whose contract is "take state, return new state". The repository topics list reducer, redux and state-tree, which matches that framing: the library is aimed at state containers and at the reducer functions that feed them, not at general-purpose data structures.
The README states the promise in one line: create the next immutable state tree by simply modifying the current tree. Everything else in the project follows from that sentence, including the parts that are awkward.
How produce() works: drafts, proxies and structural sharing
The core export is produce. You pass it a base state and a recipe, and inside the recipe the value you receive is a draft. Writes to the draft are recorded rather than applied to the original. When the recipe returns, produce walks the recorded changes and builds the next state, reusing the untouched branches of the base by reference. That reuse is the structural sharing that makes the result cheap to compare: an unchanged subtree is the same object identity as before.
Two consequences follow from the draft being a proxy. First, reads inside the recipe are proxied too, which is where the cost sits. Second, the draft is only valid during the synchronous execution of the recipe. The documentation is explicit that you must not keep a reference to the draft and use it later, and that the recipe should not be asynchronous. If you need to await something, compute the value first and then call produce.
The library is distributed in several module formats. The package.json exposes a react-native condition pointing at dist/immer.legacy-esm.js, an import condition pointing at dist/immer.mjs, and a require condition pointing at dist/cjs/index.js, with types at dist/immer.d.ts. Optional capabilities such as Map and Set support, patches, and array methods are not in the default build; the package scripts reference enableMapSet, enablePatches and enableArrayMethods as separate entry points, which tells you they are opt-in plugins rather than always-on features.
Installing Immer and a first produce() call
The README points at the npm package for installation and at the documentation site, immerjs.github.io/immer, for the API. The package name is immer, and the repository's package.json lists the build entry points described above. There is no install command or usage example in the README itself, so the first real use is best read from the documentation site rather than reconstructed here.
What the repository does tell you is how the package is consumed. The exports map in package.json routes an import to dist/immer.mjs, a require to dist/cjs/index.js, and a react-native resolver to dist/immer.legacy-esm.js, with dist/immer.d.ts supplying types for all three.
".": {
"react-native": {
"types": "./dist/immer.d.ts",
"default": "./dist/immer.legacy-esm.js"
},
"import": {
"types": "./dist/immer.d.ts",
"default": "./dist/immer.mjs"
},
"require": {
"types": "./dist/immer.d.ts",
"default": "./dist/cjs/index.js"
}
}That block is the relevant part of the manifest, not an example of calling the library. The practical check when you wire Immer into a project is which of those three files your bundler resolves, because the react-native path and the web path are not the same file. Optional features follow the same pattern: the package scripts name enableMapSet, enablePatches and enableArrayMethods as separate entry points, so Map and Set support, patches and array methods are pulled in explicitly rather than being part of the default import.
Where Immer is the wrong tool
The proxy-based draft is the source of the main limitations, and they are worth stating plainly rather than treating as footnotes.
First, the recipe is synchronous. If your update logic needs to fetch something before it can decide what to change, produce is the wrong place to put that logic. You compute the value outside and pass the result in.
Second, holding a draft past the end of the recipe is a bug, not a style choice. The documentation warns against keeping references to the draft, because the proxy is tied to the produce call that created it.
Third, the proxy cost is real. For a store that updates on every keystroke, wrapping each update in produce adds work that a hand-written spread would not. The repository ships a perf-testing directory and a __performance_tests__ directory, which indicates the maintainers treat performance as something to measure, but the README does not publish numbers, so treat any claim about speed as something you have to measure against your own update shape.
Fourth, if your state is flat and your updates are one level deep, Immer is overhead with no payoff. A single spread is shorter, faster, and has no runtime dependency.
Finally, the freezing behaviour that catches accidental mutation is a development aid. The package.json marks the module as sideEffects: false, and the build is split between development and production paths, which is the usual arrangement for a library that does extra work in development and strips it out for production. Confirm which build your bundler picks before you rely on freeze errors appearing in a deployed environment.
Immer versus Immutable.js: two different answers to the same question
Immutable.js is the comparison people reach for, and the difference is architectural rather than cosmetic. Immutable.js gives you its own collection types: List, Map, and so on. Your state is not a plain JavaScript object, it is an Immutable.js structure, and you read it through that library's API. The benefit is that the data structure itself is built for persistent updates, so the cost of an update does not depend on a proxy intercepting your writes.
The cost is that everything touching that state has to know about Immutable.js. Serializing it, logging it, passing it to a component that expects a plain object, all of that needs conversion. Immer takes the opposite trade: your state stays a plain object or array, and the library does its work at the moment of the update. Nothing downstream needs to know Immer was involved.
That is the real decision. If you want a data structure that is persistent by construction and you control everything that reads it, Immutable.js is coherent. If you want plain objects to stay plain objects and you are willing to pay a per-update proxy cost to get there, Immer is the fit. The related searches that pair the two names are asking exactly this, and there is no answer that is right for both codebases.
TypeScript, React Native and the build you actually get
Types ship with the package at dist/immer.d.ts, and the exports map declares that path for the react-native, import and require conditions alike. So a TypeScript project that resolves the package normally should get the same types regardless of module system.
React Native is handled explicitly. The react-native condition in the exports map points at dist/immer.legacy-esm.js rather than the modern dist/immer.mjs. That is a deliberate fallback, and it means a React Native bundle and a web bundle can end up on different files. If you are debugging a difference in behaviour between the two, check which file each bundler resolved before you look anywhere else.
The package.json also declares jsnext:main and source fields pointing at dist/immer.mjs and src/immer.ts. Those are the entry points some bundlers prefer. Between the exports map, the legacy fallback and jsnext:main, there are enough resolution paths that a misconfigured bundler can silently pick one you did not intend. The version field in the repository's package.json reads 10.0.3-beta while the recent releases list v11.1.18, so the checked-in manifest and the published line do not agree; read the release notes rather than the manifest when you need the current version.
Maintenance, licensing and what an upgrade costs
The repository is not archived, and the last push was on 2026-09-12. Releases have continued through the v11.1.x line, with v11.1.18 published on 2026-08-19, v11.1.17 on 2026-08-16 and v11.1.16 on 2026-08-05. That is a steady cadence over roughly two weeks, and the release process is automated: the package scripts run semantic-release against the main branch, and a pre-commit hook runs pretty-quick on staged files. A project that publishes through semantic-release and enforces formatting on commit is signalling that it intends to keep releasing, though the README itself does not describe a support window or a deprecation policy, so there is no stated commitment about how long a given major line will receive fixes.
The licence is MIT. That is permissive: it allows use in closed-source products, modification, and redistribution, provided the copyright notice and permission notice are kept. It does not grant patent rights and it comes with no warranty, which matters if you are relying on it in a regulated context. This is a description of the licence text, not legal advice; if the distinction matters to your organisation, have counsel read the LICENSE file rather than this paragraph.
Upgrade cost is dominated by the build surface rather than the API. Because the package exposes separate entry points for Map and Set support, patches and array methods, a major upgrade can change what those entry points export without changing produce itself. The repository ships a build test (test:build) that runs the suite against the built output, which suggests the maintainers treat the dist files as a contract worth testing. If you pin Immer, pin the entry points you import, not just the package version.
Editorial conclusion
Adopt Immer when your reducers or store updates already read like a list of assignments and you want those assignments to stay in place while the surrounding tree stays immutable. Skip it when your updates are already a single shallow spread, or when you cannot accept a Proxy on the hot path and a production build that relies on freezing in development. Before you commit, verify two things in your own code: that your toolchain resolves the modern ESM build rather than the legacy fallback, and that the TypeScript types you get come from dist/immer.d.ts and not from a stale copy bundled by another package. Those two checks decide whether Immer behaves the way the documentation describes in your app.
Frequently asked questions
What does Immer do?
Immer creates the next immutable state tree from the current one. You pass a base state and a recipe to produce, mutate the draft inside the recipe, and get back a new state that reuses unchanged branches by reference.
What are the alternatives to Immer for JavaScript state?
Immutable.js is the closest comparison and takes the opposite approach: it supplies its own persistent collection types such as List and Map, so state is not a plain object and readers must use its API. Immer keeps state as plain objects and arrays and does the work at update time.
What is the alternative to Immer in React?
In React the same trade applies. Immutable.js keeps persistent structures that components must read through its API, while Immer leaves the state as plain objects so components need no knowledge of it. Immer's own documentation is hosted at immerjs.github.io/immer.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/immerjs-immer)