immutable review
Immutable.js 5.1.9 provides persistent List, Map, Set, OrderedMap, Stack, Record, Range, and lazy Seq collections. Each edit returns a new value while unchanged branches share memory, and collections compare by contents through `equals()` or `is()`. That model helps history, snapshots, and memoized selectors, but it introduces collection methods and conversion boundaries that native arrays and objects do not have. Version 5.1.9 fixes a concrete List corruption bug: an `undefined` entry could become `null` after the list grew beyond 32 elements. Our full browser import measured 66.8 KB minified and 18.9 KB gzipped.
Immutable 5.1.9 installed as one 1 MB package in 0.6 seconds in our sandbox, but a full import added 66.8 KB minified and 18.9 KB gzipped. Install it for persistent collection semantics you will use throughout a subsystem; for ordinary UI state, Immer or native updates keep boundaries easier to read.
We installed it
| Install | ✓ · 0.6s | 1 package on disk · 1 MB |
| Import | ✓ | ESM import works · require() works · CommonJS package |
| Browser | 18.9 KB | gzipped (66.8 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does immutable install cleanly?
Yes. In a fresh container with an empty cache, npm install immutable finished in 0.6s, leaving 1 package and 1 MB on disk. npm audit reported no known vulnerabilities.
How much does immutable add to a browser bundle?
18.9 KB gzipped (66.8 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does immutable work with both ESM and CommonJS?
Yes. Both import 'immutable' and require('immutable') worked in Node 22 in our run. The package is published as CommonJS.
Does immutable include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
immutable or immer: which should you use?
immer: Use it when immutable updates should preserve ordinary JavaScript objects and arrays. Immutable 5.1.9 installed as one 1 MB package in 0.6 seconds in our sandbox, but a full import added 66.8 KB minified and 18.9 KB gzipped.
When should you not use immutable?
Most application state is small JSON-shaped data. Immer keeps native objects and arrays, avoiding Immutable conversion at component and API edges.
Use it if
- An editor or state engine retains many versions of large collections and deep copying has become measurable.
- Maps or Sets need collection values as keys with content-based equality and hashes.
- Selectors can exploit the original reference returned by an update that changes no value.
- A long or infinite Range should pass through lazy map, filter, take, and materialization steps.
- Most application state is small JSON-shaped data. Immer keeps native objects and arrays, avoiding Immutable conversion at component and API edges.
- Data enters and leaves charts, forms, DOM code, or network handlers on every operation. Repeated `fromJS()` and `toJS()` calls copy data and hide the sharing benefit.
- A 66.8 KB minified and 18.9 KB gzipped full import exceeds the page budget for the collection operations you need.
- The team does not want to learn a parallel collection model with `getIn()`, lazy Seq evaluation, Record factories, and value equality.
- A typed v4 application cannot budget for a migration. Version 5 removes the default export, tightens Map inference, drops deprecated statics, and requires both Range bounds.
Setup reality
We installed immutable 5.1.9 in a fresh Node 22 Bookworm sandbox in 0.6 seconds. It left one package and 1 MB on disk. The package has 0 direct dependencies, 0 peer dependencies, and a 732 KB unpacked size; npm audit found 0 vulnerabilities. It includes TypeScript declarations. Although its metadata describes a CommonJS package without an exports map, both require() and ESM import worked in our checks. A full esbuild import produced 66.8 KB minified and 18.9 KB gzipped.
No credential, native compiler, or config file is involved. Type declarations need ES2015 library types for iterators. Version 5 uses named imports such as import { Map } from 'immutable'; the previous default export is gone. More precise Map inference can expose v4 code that mixed object keys and values. An endless Range now has to spell out both bounds, for example Range(0, Infinity).
Set conversion rules before adopting the library across a codebase. fromJS() recursively replaces objects and arrays with Map and List. toJS() recursively creates fresh plain data, so using it inside React renders or cached selectors breaks the reference checks you adopted Immutable for. toObject() and toArray() are shallow. Object keys were strings before conversion, which means fromJS({1: 'x'}).get(1) does not find the stored key.
Seq evaluates lazily and keeps no result cache. Two iterations perform the pipeline twice; call toList() when the result will be reused. withMutations() speeds a supported group of writes through a temporary mutable view, but ignoring a method that returns a separate collection loses that update. Use equals() or is() for content equality. A matching === proves no value changed; two different references may still contain equal data.
Patterns
Change one Map value update-map
import { Map } from 'immutable';
const before = Map({ status: 'draft', count: 1 });
const after = before.set('status', 'ready');
console.log(before.get('status'));
console.log(after.get('status'));When `set()` receives the value already stored at that key, it returns the original Map reference.
Import version 5 collections import-v5
import { List, Map, Record } from 'immutable';
const ids = List([1, 2]);
const flags = Map({ active: true });Version 5 has no default export. Named imports are the direct migration target; a namespace import can preserve `Immutable.Map` call sites temporarily.
Test two collections by contents compare-values
import { Map, is } from 'immutable';
const left = Map({ id: 7 });
const right = Map({ id: 7 });
console.log(left === right);
console.log(left.equals(right));
console.log(is(left, right));`===` checks identity. Use `equals()` or `is()` when two separately constructed collections should compare by their values.
Convert a nested payload once convert-nested-data
import { fromJS } from 'immutable';
const state = fromJS({
users: { u1: { tags: ['admin'] } },
});
const next = state.updateIn(
['users', 'u1', 'tags'],
(tags) => tags.push('owner'),
);`fromJS()` converts the nested tags array into a List, so later writes must use List methods such as `push()`.
Read a nested path with a fallback read-nested-value
const theme = state.getIn(
['preferences', 'theme'],
'system',
);The fallback applies to a missing path. If the path exists and stores `undefined`, the result remains `undefined`.
Group supported List writes batch-updates
import { List } from 'immutable';
const before = List([1, 2, 3]);
const after = before.withMutations((draft) => {
draft.push(4);
draft.set(0, 10);
draft.pop();
});Only methods documented for `withMutations()` are safe here. Keep the returned collection from any operation that produces a separate value.
Create a fixed Record factory define-record
import { Record } from 'immutable';
const Settings = Record({ theme: 'light', fontSize: 14 });
const first = Settings();
const second = first.set('theme', 'dark');
console.log(second.theme);A Record accepts only its declared keys and supplies defaults. Give TypeScript an explicit instance type when property access must be checked.
Merge inside nested collections merge-nested
import { fromJS } from 'immutable';
const base = fromJS({ prefs: { theme: 'light', size: 14 } });
const next = base.mergeDeep({ prefs: { size: 16 } });
console.log(next.getIn(['prefs', 'theme']));`merge()` replaces at its current level; `mergeDeep()` descends into compatible nested collections.
Bound an infinite Range build-lazy-sequence
import { Range } from 'immutable';
const values = Range(0, Infinity)
.filter((value) => value % 2 === 0)
.map((value) => value * value)
.take(5)
.toList();Version 5 expects start and end arguments. Apply `take()` or another finite operation before realizing `Infinity` into a strict collection.
Materialize a reused Seq pipeline materialize-seq
import { Seq } from 'immutable';
const pipeline = Seq(source)
.filter(isEligible)
.map(toSummary);
const summaries = pipeline.toList();
renderList(summaries);
writeReport(summaries);Seq does not cache evaluated items. `toList()` performs the pipeline once so both consumers reuse the same result.
Convert only the public subtree serialize-boundary
const responseBody = state.get('publicData').toJS();
res.json(responseBody);`toJS()` copies recursively. Selecting the response subtree first prevents an API handler from copying unrelated application state.
Remember partition result order partition-list
import { List } from 'immutable';
const numbers = List([1, 2, 3, 4, 5, 6]);
const [odd, even] = numbers.partition((value) => value % 2 === 0);`partition()` returns rejected values first and accepted values second. Name the tuple entries so this ordering is visible in code review.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| immer | npm | Use it when immutable updates should preserve ordinary JavaScript objects and arrays. |
| mutative | npm | Use it for a draft-style update API after benchmarking the hot state paths that matter. |
| @reduxjs/toolkit | npm | Use it when Redux store structure, reducers, and async flows are the actual problem to solve. |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

