zustand-mutative

A Mutative middleware for Zustand that turns immutable state updates into simple draft mutations.

Library
npm
v1.3.1
121stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
32/100Needs Attention
Development Activity0
Maintenance32
Community28
Maturity48
Momentum20

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
56/100Fair
Architecture78
Code Quality70
Innovation52
Learning Curve25

zustand-mutative is a lightweight middleware that plugs Mutative’s draft-based update model into Zustand’s setState, letting store actions mutate a draft directly instead of spreading nested objects or arrays by hand. Every mutation still produces a new immutable state object under the hood, so Zustand’s reference-equality change detection and React re-render behavior keep working exactly as before — the middleware only changes how developers write updates, not how Zustand consumes them.

Where Zustand’s own immer middleware relies on Immer’s proxy-based drafting, this package swaps in Mutative, which the project’s own benchmarks show running roughly 2-6x faster than plain spread updates and more than 10x faster than Immer for large object/array state. The implementation is a single TypeScript file that fully types the augmented setState signature so mutative()-wrapped stores keep the same type inference and middleware composition guarantees as any other Zustand store.

What You Get

  • Draft-based setState - write state.count += 1 inside updates instead of spreading nested state manually
  • Full TypeScript typing - augments Zustand’s StoreMutators interface so composed middleware chains keep correct type inference
  • Mutative-powered performance - benchmarked 2-6x faster than spread-based updates and 10x+ faster than the Immer middleware for large state
  • Drop-in middleware - wraps any StateCreator the same way immer() or persist() do, composable with other Zustand middleware
  • Configurable via Mutative options - strict mode, auto-freeze, and custom data-structure marking are passed straight through to Mutative’s create()

Common Use Cases

  • Migrating a Zustand store off manual spread updates when nested state (arrays of objects, deep records) becomes error-prone to update immutably by hand
  • Replacing the built-in immer middleware in performance-sensitive stores with large arrays or objects where Mutative’s speed advantage matters
  • Building stores with deeply nested domain state (forms, normalized entity maps, undo/redo buffers) where draft mutation reads more naturally than spread syntax
  • Any Zustand application wanting Immer-like ergonomics without pulling in Immer as a dependency

Under The Hood

Architecture The entire middleware is one exported function, mutative(), implemented in src/index.ts by reassigning store.setState: when a Zustand store is created with create<T>()(mutative((set, get, store) => ...)), the middleware intercepts the store before the user’s initializer ever runs, replaces store.setState with a wrapper that detects function updaters and runs them through Mutative’s create() (with enablePatches forced off, since Zustand doesn’t consume patches), and then calls through to the original set with the resulting next state. Plain object/partial updaters pass through untouched. The initializer is invoked with this patched setState so every set() call made inside store actions is drafted through Mutative automatically. This is the same interception pattern Zustand’s own bundled immer middleware uses, extended with a set of mapped TypeScript types (Write, SkipTwo, a StoreMutators['zustand/mutative'] module augmentation) so the mutated signature composes correctly with other middleware in a chain.

Tech Stack Written in strict-mode TypeScript with mutative and zustand as peer dependencies (supporting Zustand v4 and v5). The library is bundled with Rollup into CJS, ESM, and UMD outputs plus separate .d.ts/.d.mts type declarations for dual-package interop, and published to npm through a GitHub Actions workflow that runs the build and test suite before publishing with npm provenance. A scripts/benchmark.ts harness using the benchmark package and quickchart-js generates the performance chart checked into the repo.

Code Quality Tests run under Jest with ts-jest and a jsdom environment; index.test.ts covers runtime update behavior via @testing-library/react’s renderHook/act, while a much larger middlewareType.test.tsx is almost entirely compile-time type assertions checking that middleware composition (with persist, devtools, subscribeWithSelector, etc.) still infers correctly — an appropriate emphasis given the library’s main risk surface is its TypeScript typings rather than runtime logic, though actual behavioral test count is limited. ESLint extends airbnb plus @typescript-eslint recommended rules with Prettier integration, and tsconfig.json enables full strict type-checking. CI runs the install/build/test cycle across Node 18, 20, and 22 on every push, and the same gate runs again before any npm release.

What Makes It Unique Rather than introducing a new state-management paradigm, zustand-mutative mirrors the shape of Zustand’s existing built-in immer middleware but swaps in Mutative as the drafting engine, making it close to a drop-in replacement for teams already using that pattern. The differentiator is purely performance: Mutative avoids some of the proxy-drafting overhead Immer incurs, and the project’s own benchmarks (checked into the repo) show a substantial speedup on large array/object state updates — a focused, well-scoped optimization for a common Zustand pain point rather than a novel abstraction.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search