RepoPilot

immerjs/immer

Create the next immutable state by mutating the current one

Healthy

Strong maintenance signals

HealthyDependency

Permissive license, no critical CVEs, actively maintained — safe to depend on.

HealthyFork & modify

Has a license, tests, and CI — clean foundation to fork and modify.

HealthyLearn from

Documented and popular — useful reference codebase to read through.

MixedDeploy as-is

Scorecard "Branch-Protection" is 0/10; Scorecard "Token-Permissions" is 0/10

  • Scorecard: default branch unprotected (0/10)
  • Last commit 2d ago
  • 10 active contributors
  • Distributed ownership (top contributor 41% of recent commits)
  • MIT licensed
  • CI configured
  • Tests present

What would improve this?

  • Deploy as-is Mixed to Healthy if: bring "Branch-Protection" to ≥3/10 (see scorecard report)

Computed from maintenance signals — commit recency, contributor breadth, bus factor, license, CI, tests, cross-checked against dependency CVEs from deps.dev and OpenSSF Scorecard

Informational only. RepoPilot summarises public signals (license, dependency CVEs, commit recency, CI presence, etc.) at the time of analysis. Signals can be incomplete or stale. Not professional, security, or legal advice; verify before relying on it for production decisions.

Want this for your own repo?

Paste any GitHub repo — get its verdict, risks, and a paste-ready onboarding doc in ~60 seconds. Free, no sign-up.

Embed the "Healthy" badge

Paste into your README — live-updates from the latest cached analysis.

Variant:
RepoPilot: Healthy
[![RepoPilot: Healthy](https://repopilot.app/api/badge/immerjs/immer)](https://repopilot.app/r/immerjs/immer)

Paste at the top of your README.md — renders inline like a shields.io badge.

Preview social card

This card auto-renders when someone shares https://repopilot.app/r/immerjs/immer on X, Slack, or LinkedIn.

Ask AI about immerjs/immer

Grounded in the actual source code. Pick a starter question or write your own.

Or write your own question

Onboarding doc

Onboarding: immerjs/immer

Generated by RepoPilot · 2026-07-05 · Source

Verdict

Healthy — Strong maintenance signals

  • Last commit 2d ago
  • 10 active contributors
  • Distributed ownership (top contributor 41% of recent commits)
  • MIT licensed
  • CI configured
  • Tests present
  • ⚠ Scorecard: default branch unprotected (0/10)

Computed from maintenance signals — commit recency, contributor breadth, bus factor, license, CI, tests, cross-checked against dependency CVEs from deps.dev and OpenSSF Scorecard

TL;DR

Immer is a JavaScript library that enables immutable state updates through a copy-on-write proxy system: you mutate a draft state directly, and Immer produces the next immutable state with minimal structural sharing. It eliminates nested spread operator boilerplate and enables declarative mutations that compile to efficient immutable updates, winning React open source awards in 2019 for breakthrough utility. Single-package library with monolithic src/ structure: src/core/ houses the Proxy-based mutation engine (immerClass.ts, proxy.ts, finalize.ts), src/plugins/ provide opt-in features (mapset.ts for Map/Set, patches.ts for change tracking, arrayMethods.ts for mutative array methods), and src/utils/ hold environment detection and error handling. Built with tsup, tested via Vitest, with perf-testing and website/ directories for benchmarks and docs.

LLM-derived; treat as a starting point, not verified fact.

Who it's for

React developers and Redux users who manage complex state trees and want to write mutations naturally without manual spread operators or Lens libraries. Also state management library maintainers (Zustand, MobX, Redux Toolkit) who integrate Immer to simplify their mutation APIs.

LLM-derived; treat as a starting point, not verified fact.

Maturity & risk

Production-ready and actively maintained. The project hit v10 (currently in beta as v10.0.3-beta per package.json), has comprehensive TypeScript definitions, extensive test coverage via Vitest, CI via GitHub Actions with coveralls integration, and semantic-release automation. Regular releases and ongoing optimizations indicate steady active development.

Low risk for core use cases; v10 is stable but in beta phase, so minor API changes remain possible. Single maintainer (Michel Weststrate) creates some sustainability risk, though community contributions are active. The proxy-based implementation has subtle edge cases with non-proxy-compatible objects (e.g., DOM nodes, class instances), documented in mapset.ts and arrayMethods.ts plugins. No high-risk transitive dependencies visible in package.json snippet.

LLM-derived; treat as a starting point, not verified fact.

Active areas of work

v10 beta release cycle with stability focus. Performance tests in performance_tests/ suggest ongoing optimization work (add-data.mjs, incremental.mjs benchmarks). TypeScript-first development with dual exports (ESM, CommonJS, legacy-ESM via tsup config) and React Native support via dist/immer.legacy-esm.js.

LLM-derived; treat as a starting point, not verified fact.

Get running

git clone https://github.com/immerjs/immer.git
cd immer
yarn install
yarn watch  # runs Vitest in watch mode for development
yarn build  # compiles to dist/ via tsup
yarn test   # runs full test suite including build tests and Flow checks

Daily commands:

# Development with file watching:
yarn watch

# Full test suite:
yarn test

# Build production artifacts:
yarn build

# Performance regression tests:
cd __performance_tests__ && node add-data.mjs

# Website (docs) locally:
cd website && yarn start

Map of the codebase

  • src/immer.ts — Main entry point exporting the produce function and Immer class; defines the public API.
  • src/core/immerClass.ts — Core Immer class managing draft state creation, finalization, and the immutable update lifecycle.
  • src/core/proxy.ts — Implements Proxy-based interception of mutations on draft objects to track changes.
  • src/core/finalize.ts — Converts mutable draft changes into a new immutable state tree without mutating the original.
  • src/core/scope.ts — Manages nested draft scopes and shared state across recursive produce calls.
  • src/internal.ts — Internal constants and symbols used throughout the codebase for marking drafts and metadata.
  • src/plugins/patches.ts — Plugin enabling generation of JSON patches describing changes between original and next state.

Components & responsibilities

  • Immer class (TypeScript, Proxy, scope registry) — Owns the produce lifecycle: creates drafts, invokes recipes, finalizes state, and manages scope.
    • Failure mode: Throws if recipe throws or if draft is used after produce completes.
  • Proxy handler (proxy.ts) (Proxy API, change tracking) — Intercepts all property access and mutations on draft objects; records changes and auto-vivifies nested proxies.
    • Failure mode: Silently ignores mutations if trap is not properly configured; may miss deep nested changes.
  • Finalize engine (finalize.ts) (Structural sharing, recursive tree walking) — Traverses the draft tree and applies recorded mutations to build a new immutable result tree.
    • Failure mode: Produces incorrect state if change tracking is incomplete; may leak references if finalization skips frozen checks.
  • Scope manager (scope.ts) (Global state, Map registry) — Maintains global draft registry and scope stack for managing nested produce() calls and draft lifecycle.
    • Failure mode: Memory leaks if drafts are not properly cleaned up; scope corruption if nested produces interfere.
  • Plugin system (plugins.ts, patches.ts, mapset.ts) (Hook registry, custom drafting logic) — Registers and executes optional extensions for patches, Map/Set support, and array methods via hook callbacks.
    • Failure mode: Patch generation fails if plugin is not enabled; Map/Set mutations not tracked if mapset plugin not loaded.
  • Type system (types-external.ts, types-internal.ts) (TypeScript generics and conditional types) — Provides TypeScript type definitions and internal utility types for strong typing across the codebase.
    • Failure mode: Type inference errors if draft type is not properly constrained; users lose IDE autocompletion.

Data flow

  • User code (recipe function)Proxy handlers — Recipe mutations are intercepted by proxy traps to record changes.
  • Proxy handlersScope registry — Change records are stored in the current draft scope for later finalization.
  • Scope registryFinalize engine — After recipe completes, finalize reads all recorded changes and builds next state.
  • Finalize enginePlugin hooks (finalize callbacks) — Plugins can customize finalization behavior for custom types like Map, Set, patches.
  • Finalize engineResult (immutable state tree) — Finalized state is returned to user; original baseState remains unchanged.

How to make changes

Add a new plugin

  1. Create a new plugin file in src/plugins/ that exports an enablePlugin function (src/plugins/yourplugin.ts)
  2. Implement hook functions (e.g., onCopy, onFinalize) using the plugin API (src/plugins/yourplugin.ts)
  3. Register the plugin using enablePlugin() from src/utils/plugins.ts (src/utils/plugins.ts)
  4. Export the plugin function from src/immer.ts or create a separate entry point (src/immer.ts)
  5. Add test coverage in tests/plugins.js (__tests__/plugins.js)

Extend draft object behavior for a new collection type

  1. Add type detection and drafting logic in src/core/proxy.ts (src/core/proxy.ts)
  2. Create a plugin similar to src/plugins/mapset.ts to handle mutation interception (src/plugins/yourtype.ts)
  3. Register custom copy and finalization handlers via the plugin hook system (src/plugins/yourtype.ts)
  4. Add comprehensive tests to tests directory (__tests__/yourtype.js)

Add a new public utility function

  1. Implement the utility function in src/immer.ts or a new file in src/core/ (src/immer.ts)
  2. Export the function from src/immer.ts for public consumption (src/immer.ts)
  3. Add TypeScript definitions to src/types/types-external.ts (src/types/types-external.ts)
  4. Add unit tests to the relevant test file in tests/ (__tests__/base.js)

Why these technologies

  • JavaScript Proxy — Intercepts property access and mutations on draft objects to automatically track changes without explicit tracking calls.
  • TypeScript — Provides strong typing for public API and supports multiple output formats (ESM, CommonJS, Flow) for different consumers.
  • Plugin system — Allows optional extensions (patches, Map/Set, array methods) without bloating the core bundle for users who don't need them.
  • Structural sharing — Reuses unchanged portions of the object tree in the result, minimizing memory overhead and preserving referential equality for unchanged branches.

Trade-offs already made

  • Proxy-based instead of immutable.js style persistent data structures

    • Why: Proxies provide a more natural, mutation-like API while relying on structural sharing.
    • Consequence: Simpler user experience but requires fallback drafting strategy for older environments without Proxy support.
  • Single-threaded, synchronous design with no async support in recipes

    • Why: Ensures predictable state finalization and simpler mental model for developers.
    • Consequence: Cannot be used directly with async/await mutations; users must finalize before async operations.
  • Scope-based draft registry instead of WeakMap everywhere

    • Why: Allows tracking of nested produce() calls and multi-level draft hierarchies efficiently.
    • Consequence: Increases memory footprint slightly during deeply nested produce calls but enables correct semantics.

Non-goals (don't propose these)

  • Does not provide real-time collaboration or CRDT synchronization
  • Does not support browser storage persistence (localStorage, IndexedDB)
  • Not designed for distributed systems or network protocol implementation
  • Does not include built-in time-travel debugging (though patches enable it externally)

Code metrics

  • Avg cyclomatic complexity: ~6.5 — Core engine combines Proxy interception, recursive tree walking, and scope management; plugins add conditional complexity branches.
  • Largest file: src/core/proxy.ts (420 lines)
  • Estimated quality issues: ~3 — Minor issues: some large functions in proxy.ts could be refactored; scope.ts has global state; finalize.ts has deep recursion without tail-call optimization.

Anti-patterns to avoid

  • Draft escape (High)src/core/immerClass.ts, __tests__/base.js: Returning a draft object from a recipe function bypasses finalization and can cause stale mutable references; caught at runtime with warnings.
  • Async mutations in recipe (High)README, src/core/immerClass.ts: Using async/await inside a recipe function can finalize state before async operations complete, leading to incorrect updates.
  • Implicit array mutation without tracking (Medium)src/plugins/arrayMethods.ts: Direct array index assignment or length modification may not be tracked properly without the arrayMethods plugin enabled.
  • Scope pollution from nested produces (Medium)src/core/scope.ts: Deeply nested produce() calls can accumulate drafts in the scope stack; potential memory leaks if drafts not finalized.

Performance hotspots

  • src/core/finalize.ts (recursive tree walking) (CPU-bound) — Finalization recursively traverses the entire draft tree; becomes slow for very large objects with thousands of properties.
  • src/core/proxy.ts (trap invocation) (Runtime overhead) — Every property access and mutation invokes a Proxy trap; high-frequency property accesses accumulate overhead.
  • src/core/scope.ts (global scope stack) (Concurrency bottleneck) — Single global scope stack is shared across all concurrent produce() calls; contention in high-concurrency scenarios.

Traps & gotchas

Proxies do not work with non-extensible objects, frozen objects, or DOM nodes—see mapset.ts for WeakMap workarounds and docs on limitations. The env.ts module detects Proxy support and falls back gracefully, but code relying on non-mutative semantics (e.g., === identity checks in drafts) will fail silently. Array.length assignment and sparse array indices have quirks handled in arrayMethods.ts; review before extending. Patches are only generated if enablePatches() is called before produce()—plugin setup order matters. TypeScript consumers must use TypeScript 3.7+ for distributive conditional types to infer union types correctly in Immer's Draft<T> type.

Architecture

Concepts to learn

  • Copy-on-write (CoW) — Immer's core strategy: mutations are recorded during drafting, then only modified objects and their ancestors are cloned in the final state, leaving unchanged subtrees shared; understanding this is essential to predict performance and memory usage
  • ES6 Proxy — Immer wraps draft state in Proxies to intercept property access and mutations without modifying the original; Proxy traps (get, set, deleteProperty, has) are the mechanics behind draft tracking
  • Structural sharing — Immer reuses unmodified parent/sibling objects in the output tree to minimize allocations; this is why an unchanged array element costs zero bytes, and is the foundation of O(1) referential equality checks for unmodified subtrees
  • JSON Patch (RFC 6902) — Immer's patches.ts plugin generates patches in standard RFC 6902 format (op, path, value) enabling portable undo/redo and state synchronization between clients; understanding patch semantics is crucial for conflict-free merges
  • Scope graph (draft tracking) — Immer maintains a scope tree in scope.ts to track which objects are currently being drafted and their parent/child relationships; this enables finalize.ts to determine the minimal set of objects to clone, avoiding deep traversals
  • Immutatibility invariant — Once produce() returns, the output state is considered frozen; Immer relies on user promise not to mutate it, because structural sharing means mutation would corrupt sibling branches—violations are hard to debug
  • Autofreezing — Immer's finalize.ts can optionally freeze output (and all nested objects) via Object.freeze(); this catches accidental mutations at runtime but has perf cost; understanding when to enable/disable freezing is important for production tuning
  • redux-toolkit/redux-toolkit — Official Redux state management integration that bundles Immer as core to enable createSlice() to mutate draft state; heavily used together
  • pmndrs/zustand — Lightweight state management library that integrates Immer via immer middleware for mutation-based state updates; alternative to Immer-in-Redux
  • mobxjs/mobx — Alternative reactive state management that supports mutations natively; often compared to Immer in discussions of mutation models
  • immerjs/immer-produce — Official Immer plugin/example repository showing advanced patterns like async producers and integration with async-await
  • seamless-immutable/seamless-immutable — Predecessor immutable library using Object.freeze + getters; Immer's Proxy-based approach is more performant successor

PR ideas

Click to expand

To work on one of these in Claude Code or Cursor, paste: Implement the "<title>" PR idea from CLAUDE.md, working through the checklist as the task list.

Add comprehensive test coverage for src/core/scope.ts

scope.ts is a critical internal module managing draft state lifecycle and scope management, but there are no dedicated unit tests in tests specifically targeting its functions. This is a high-risk file for regressions since it handles core state management logic. Adding focused tests would improve reliability for scope creation, cleanup, and nested draft handling.

  • [ ] Create tests/scope.test.ts with tests for scope stack management
  • [ ] Add tests for draft lifecycle (creation, finalization, cleanup)
  • [ ] Test nested scope scenarios and edge cases
  • [ ] Test scope integration with immerClass.ts produce() calls
  • [ ] Verify coverage meets project threshold (currently using vitest with coverage)

Add plugin architecture documentation with working examples in tests

The repo has src/plugins/ (arrayMethods.ts, mapset.ts, patches.ts) and src/utils/plugins.ts showing a plugin system, but there are no documented examples or test cases showing how external users can create custom plugins. The tests directory lacks plugin-specific test scenarios that could serve as reference implementations.

  • [ ] Create tests/plugins.test.ts demonstrating plugin registration and lifecycle
  • [ ] Add tests showing how to extend Immer with custom types (similar to MapSet pattern)
  • [ ] Document the plugin interface by creating a reference example in tests/fixtures/customPlugin.ts
  • [ ] Test plugin interaction with patches and arrayMethods to ensure composability
  • [ ] Add JSDoc comments to src/utils/plugins.ts explaining the API

Add performance regression tests for proxy.ts operations

The performance_tests directory exists but uses manual .mjs scripts. The core src/core/proxy.ts file is performance-critical (handles all draft object interception), yet there are no automated performance benchmarks in CI. This creates risk for unintended performance regressions in proxy trap operations.

  • [ ] Create vitest benchmark suite in tests/perf/proxy.bench.ts using vitest bench API
  • [ ] Benchmark common operations: property get/set, array mutations, nested object modifications
  • [ ] Add GitHub Actions workflow (.github/workflows/perf.yml) to track performance metrics on PR
  • [ ] Establish baseline metrics and failure thresholds for acceptable regression tolerance
  • [ ] Integrate results into pull request checks to alert on performance degradation

Good first issues

  • Add TypeScript JSDoc examples to src/core/current.ts (currently no comments)—show how to call current(draft) to read frozen values during drafting, matching docs/current.md examples: small
  • Extend tests/ with a dedicated test file for edge cases in arrayMethods.ts (splice, reverse on sparse arrays)—coverage gaps exist for non-contiguous mutations, which could prevent regressions when optimizing array handling: medium
  • Create a diagnostic tool in perf-testing/ that profiles proxy overhead vs. direct mutation on large nested objects (e.g., 10k-node trees)—current benchmarks test throughput but not memory usage or GC pressure during drafting: medium

Top contributors

Click to expand

Recent commits

Click to expand
  • 60ca295 — chore(deps): bump @sigstore/core from 3.2.0 to 3.2.1 (#1258) (dependabot[bot])
  • 858d036 — fix: improve DraftMap.{entries,values}() compatibility#1228 (#1228) (mrcljx)
  • 89acf94 — chore(deps): bump http-proxy-middleware from 2.0.9 to 2.0.10 in /website (#1257) (dependabot[bot])
  • f2b409e — chore(deps-dev): bump vite from 6.4.2 to 6.4.3 (#1256) (dependabot[bot])
  • e6eb6e3 — chore(deps): bump @babel/plugin-transform-modules-systemjs in /website (#1238) (dependabot[bot])
  • 5868ec5 — chore(deps): bump fast-uri from 3.1.0 to 3.1.2 in /website (#1237) (dependabot[bot])
  • c6ce0e9 — chore(deps): bump tar from 7.5.13 to 7.5.16 (#1253) (dependabot[bot])
  • 9ec6559 — chore(deps): bump joi from 17.7.0 to 17.13.4 in /website (#1250) (dependabot[bot])
  • 86c3776 — chore(deps): bump shell-quote from 1.7.4 to 1.8.4 in /website (#1248) (dependabot[bot])
  • e606b33 — chore(deps): bump shell-quote from 1.8.0 to 1.8.4 (#1247) (dependabot[bot])

Security observations

Click to expand

The Immer codebase demonstrates good security hygiene with no obvious injection vulnerabilities, hardcoded secrets, or critical misconfigurations visible. It is a pure functional library with minimal attack surface. However, the minimal security policy regarding version support and lack of documented security guidelines present moderate concerns for production users. The truncated package.json prevents full dependency analysis. Recommendations include establishing a clearer security policy, implementing automated dependency scanning, and providing security best practices documentation for users.

  • Medium · Minimal Security Policy — SECURITY.md. The SECURITY.md file indicates that only the latest version is supported and no guaranteed follow-up or remediation timelines are provided. This is a significant limitation for production users who may need security patches for older versions. Fix: Establish a clear security policy with defined support windows, SLA for vulnerability fixes, and consider maintaining security patches for at least the previous 1-2 major versions.
  • Low · Missing Security Headers Documentation — README and documentation. No documented security considerations or best practices for library consumers are provided in the repository. This could lead to improper usage patterns. Fix: Add a security considerations section to the README explaining potential misuse scenarios and best practices for using Immer securely in production applications.
  • Low · Incomplete Package.json in Analysis — package.json. The package.json file appears truncated in the provided content, making a complete dependency audit impossible. Unverified or outdated dependencies could introduce vulnerabilities. Fix: Ensure all dependencies are regularly audited using 'npm audit' or 'yarn audit'. Pin dependency versions and implement automated dependency vulnerability scanning in the CI/CD pipeline.

LLM-derived; treat as a starting point, not a security audit.

The exported doc (Copy CLAUDE.md / Download / .cursor/rules) also includes an agent protocol and a verification script written for AI coding agents — omitted here to keep this view scannable.

Embed this chat in your README

Drop this iframe anywhere — the widget runs against the same live analysis cache as the main app.

<iframe
  src="https://repopilot.app/embed/immerjs/immer"
  width="100%" height="500"
  style="border:1px solid #d0d7de; border-radius:8px;"
  allow="microphone"
  loading="lazy"
></iframe>