Immer

repository·main·Indexed 12 days ago

https://github.com/immerjs/immer

A library for creating the next immutable state by mutating a current draft tree using a proxy-based mechanism. Version 10.0.3-beta includes the produce() function for state transitions, produceWithPatches() for change tracking, and applyPatches() for state updates. It provides utilities like current(), original(), and isDraft(), as well as plugins for Map, Set, and optimized array methods via enableArrayMethods().

Tokens
25.7K
Snippets
71
Records
102
Agent score
96%

What's inside Immer

  1. What is Immer

    main
    Immer is a tiny library (approx. 3KB gzipped) that simplifies working with immutable state. It allows you to use standard, mutable-style JavaScript operations on a temporary draft object, which Immer then uses to produce a new, immutable state through structural sharing. This prevents the need for manual shallow copying (using spread operators ...) at every level of a nested data structure.
  2. Overview of Immer Performance Testing tools

    main

    The performance testing suite includes several key components:

    • immutability-benchmarks.mjs: The main script used to compare different Immer versions.
    • read-cpuprofile.js: An advanced CPU profile analyzer that supports sourcemaps.
    • rolldown.config.js: Bundler configuration used to eliminate process.env overhead during production bundling.

    Benchmarks compare the following versions:

    • immer7-10: Historical Immer versions.
    • immer10Perf: The current development version (references ../dist).
    • vanilla: Pure JavaScript implementations used as a baseline.
  3. Overview of Immer

    main
    Immer is a library designed to simplify working with immutable state. It allows you to create the next immutable state tree by simply modifying a current draft tree using a proxy-based mechanism. It is widely used in the React ecosystem for managing complex state transitions without manual spreading or deep cloning.
  4. Opt-out of Immer for performance-critical logic

    main
    Immer is opt-in. For specific parts of your logic that are extremely performance-critical, you can write manual reducers or opt-out within a producer by using the original or current utilities to perform operations on plain JavaScript objects instead of proxies.
  5. Array Methods Plugin: Callback behavior

    main

    When using enableArrayMethods(), callbacks for intercepted methods (filter, find, some, every, and slice) receive base values instead of drafts for performance reasons.

    • Reading properties from the item in the callback works fine.
    • Mutating the item inside the callback will not be tracked by Immer.

    To mutate items returned by these methods, use the result of the method call, which contains drafts.

    import {enableArrayMethods, produce} from "immer"
    enableArrayMethods()
    
    produce(state, draft => {
    	draft.items.filter(item => {
    		// `item` is a base value here, NOT a draft
    		// Reading works fine:
    		return item.value > 10
    
    		// But direct mutation here won't be tracked:
    		// item.value = 999  // ❌ Won't affect the draft!
    	})
    
    	// Instead, use the returned result (which contains drafts):
    	const filtered = draft.items.filter(item => item.value > 10)
    	filtered[0].value = 999 // ✅ This works - filtered[0] is a draft
    })
  6. Immer Terminology

    main

    Understanding these core terms helps navigate Immer documentation:

    • (base)state: The original immutable state passed as the first argument to produce.
    • recipe: The second argument of produce; a function that describes how the state should be "mutated".
    • draft: The first argument passed into the recipe function. It is a proxy to the original state that allows for safe mutations.
    • producer: A function that wraps produce, typically following the pattern (baseState, ...arguments) => resultState.
  7. How to handle async updates correctly

    main

    To avoid missing state updates during asynchronous operations, do not hold a draft open across an await. Instead, follow this pattern:

    1. Perform your asynchronous data fetching first.
    2. Once the data is received, use produce to apply the updates to your state.

    Incorrect Pattern (Anti-pattern):

    const draft = createDraft(user)
    draft.todos = await (await window.fetch("...")).json()
    const loadedUser = finishDraft(draft)

    Correct Pattern:

    const todos = await (await window.fetch("...")).json()
    const loadedUser = produce(user, draft => {
      draft.todos = todos
    })
    // Correct way: Fetch first, then produce
    const todos = await (await window.fetch("http://host/" + user.name)).json();
    const loadedUser = produce(user, draft => {
      draft.todos = todos;
    });
  8. Understand the Patch data format

    main

    Immer patches are similar to the RFC-6902 JSON patch standard, with the key difference that the path property is an array of segments rather than a string.

    Example patch structure:

    [
    	{
    		"op": "replace",
    		"path": ["profile"],
    		"value": {"name": "Veria", "age": 5}
    	},
    	{
    		"op": "remove",
    		"path": ["tags", 3]
    	}
    ]

    To normalize these to the official JSON patch specification, you can join the path array: patch.path = patch.path.join("/").

  9. Returning new data from producers

    main

    In Immer, you typically modify the draft object directly. However, you can also replace the entire state by returning a new value from the producer function.

    Rules for returning values:

    1. Modifying the draft: If you modify the draft, you do not need to return anything. Returning draft is also acceptable but redundant.
    2. Returning a new state: You can return an entirely new object to replace the current state, but only if you have not modified the draft.

    Invalid patterns to avoid:

    • Reassigning the draft: Writing draft = { ... } does nothing because it only reassigns the local variable, not the actual state.
    • Mixing mutations and returns: Modifying the draft (e.g., draft.count += 1) and then returning a new object (e.g., return { count: 2 }) is not allowed and will lead to unexpected behavior.

    For multiple changes, the 'Immer way' is to mutate the draft directly rather than constructing a new object manually.

    const userReducer = produce((draft, action) => {
    	switch (action.type) {
    		case "renameUser":
    			draft.users[action.payload.id].name = action.payload.name
    			return draft // OK: same as just 'return'
    		case "loadUsers":
    			return action.payload // OK: returns an entirely new state
    		case "adduser-1":
    			draft = {users: [...draft.users, action.payload]} // NOT OK: reassigning draft does nothing
    		case "adduser-2":
    			draft.userCount += 1
    			return {users: [...draft.users, action.payload]} // NOT OK: modifying draft AND returning new state
    		case "adduser-4":
    			draft.userCount += 1
    			draft.users.push(action.payload) // OK: the immer way (mutating draft)
    	}
    })
  10. How auto-freezing works in Immer

    main

    By default, Immer automatically freezes any state trees that are modified using produce. This mechanism protects your state tree from accidental modifications outside of a producer function.

    Key behaviors to note:

    • Recursive Freezing: Immer freezes everything recursively. For very large, static data objects, this might be inefficient. In such cases, consider using the freeze utility to shallowly pre-freeze data instead.
    • Property Scope: Immer will not freeze non-enumerable, non-own, or symbolic properties unless their content was specifically drafted.
    • Side Effects: When auto-freezing is enabled, any plain object or array included in the produced result will be frozen, even if those objects were not frozen before the producer started.