mutative

repository·main·Indexed 21 days ago

https://github.com/unadlib/mutative

A high-performance JavaScript library for efficient immutable updates. Mutative allows developers to perform immutable state management using a mutative syntax via a draft-based approach, optimizing shallow copies and avoiding default data freezing. Key features include the `create` function for state production, `apply` for patch management, and utilities like `current`, `original`, and `rawReturn` for managing drafts and performance.

Tokens
20.9K
Snippets
49
Records
110
Agent score
83%

What's inside mutative

  1. What is Mutative?

    main

    Mutative is a JavaScript library designed for efficient immutable updates. It allows you to write code using mutable syntax on a 'draft' object, which Mutative then uses to produce a new, immutable data structure. This approach simplifies state management in frameworks like React and Redux by avoiding complex spread operations and reducing the risk of accidental state mutations.

    Key benefits include:

    • Syntax conciseness: Use direct modifications on a draft object instead of manual spread/copying.
    • High performance: Significantly faster than both naive handcrafted reducers and Immer.
    • Error reduction: Prevents accidental direct mutations of the original state by isolating changes to a draft.
    • Broad support: Works with objects, arrays, Set, and Map.
  2. Explore the Mutative Ecosystem

    main

    Mutative powers several specialized libraries designed for different state management patterns and frameworks. You can use these ecosystem tools to integrate Mutative's high-performance mutable updates into your existing workflow:

    React & Hooks

    • use-mutative: A high-performance alternative to useState that uses spread operations, performing 2-6x faster.
    • use-travel: A React hook providing state time travel, including undo, redo, reset, and archive functionalities.
    • reactant: A framework specifically designed for building React applications using Mutative.

    State Management Middleware & Extensions

    • zustand-mutative: Middleware for Zustand that enhances the efficiency of immutable state updates.
    • jotai-mutative: An extension for the Jotai state management library.
    • xstate-mutative: Utilities for using Mutative with XState, offering faster and more flexible integration.
    • mutative-compat: A wrapper that provides full Immer API compatibility for Mutative.

    Time Travel & Transactional Updates

    • travels: A fast, framework-agnostic core for undo/redo powered by Mutative JSON Patch.
    • zustand-travel: A high-performance time-travel middleware specifically for Zustand.
    • mutability: A JavaScript library focused on transactional mutable updates.

    Collaborative & Universal State

    • mutative-yjs: A library for building collaborative web applications using Yjs and Mutative.
    • usm: A universal state modular library supporting Redux (5.x), MobX (6.x), Vuex (4.x), and Angular (2.0+).
  3. What is Strict Mode in Mutative?

    main

    Strict mode is a development feature designed to enforce immutability guarantees. When enabled, it ensures that state mutations adhere to immutable update patterns by preventing direct modification of the original state within a draft.

    Key benefits include:

    • Enforcing immutability: Prevents direct state mutations that cause side effects.
    • Debugging aid: Throws an immediate error if an improper mutation is attempted, helping catch bugs during development.
    • Controlled escapes: Allows developers to explicitly bypass checks using unsafe() or rawReturn() when advanced operations are required.
  4. Use strict mode in Mutative

    main
    To ensure data integrity and prevent accidental leaks of non-draft values, you can enable strict mode in your Options object. In strict mode, Mutative will throw errors if you attempt to access non-draftable values or if you attempt to return a value that is not a draft.
  5. Avoid rawReturn() when returning drafts

    main

    When performing a mutation, if your logic requires returning an object that spreads or includes parts of the current draft, do not use rawReturn(). Instead, return the object normally, and Mutative will handle the draft-to-state transition.

    If you use rawReturn() on a value containing drafts, it can lead to unexpected behavior or performance issues. It is highly recommended to enable strict: true in development to catch these instances via warnings.

    const baseState = { a: 1, b: { c: 1 } };
    const state = create(
      baseState,
      (draft) => {
        if (draft.b.c === 1) {
          // it will warn `The return value contains drafts, please don't use 'rawReturn()' to wrap the return value.` in strict mode.
          return rawReturn({
            ...draft,
            a: 2,
          });
        }
      },
      {
        strict: true,
      }
    );
    expect(state).toEqual({ a: 2, b: { c: 1 } });
    expect(isDraft(state.b)).toBeFalsy();
  6. Understand the Immutable<T> type alias

    main

    The Immutable<T> type alias is used by Mutative to represent a read-only version of a given type T. It recursively applies immutability to objects, maps, and sets.

    Depending on the input type T, Immutable<T> resolves to:

    • Primitives or AtomicObjects: The type remains T unchanged.
    • ReadonlyMap: Becomes ImmutableMap<K, V>.
    • ReadonlySet: Becomes ImmutableSet<V>.
    • Objects: Becomes ImmutableObject<T>.
    • WeakReferences: Remains T.

    This ensures that when you are working with the state produced by Mutative, TypeScript correctly enforces that you cannot perform mutations on the returned data structures.

  7. What are Patches and how are they structured

    main

    Patches are an array of operation instructions used to perform immutable updates. Each Patch object contains the following fields:

    • path: An array representing the path to the specific node in the object tree.
    • value: The new value for the object at that path.
    • op: A string representing the type of operation. Supported operations are:
      • add
      • remove
      • replace

    By applying these patches, you can perform immutable updates to an object without modifying the original source.

  8. How shared references behave in Mutative drafts

    main

    Mutative supports Directed Acyclic Graphs (DAGs) where multiple paths lead to the same node, but it treats these paths independently during the drafting process.

    When an object is referenced by multiple paths in your base state, Mutative creates independent drafts for each path. Consequently, a mutation performed on one path will not be reflected in the other paths. This behavior is intentional and matches Immer's behavior.

    Key constraints:

    • Shared nodes in base state: Each path gets its own independent draft. Mutations are path-specific.
    • Manual assignment: You can create shared references in the final state by explicitly assigning one draft to another (e.g., draft.a = draft.b).
    • Cycles: Real cycles (where a node refers back to an ancestor) are not supported and are detected in development mode when auto-freeze is enabled.
    import { create } from 'mutative';
    
    const obj = {};
    // Same object referenced by two keys
    obj.color1 = obj.color2 = { name: 'Red' };
    
    const result = create(obj, (draft) => {
      // ⚠️ Different drafts created for each path!
      console.log(draft.color1 === draft.color2); // false
    
      draft.color1.name = 'Blue';
      
      // color2 remains unchanged because it has a separate draft
      console.log(draft.color2.name); // 'Red'
    });
    
    console.log(result.color1 === result.color2); // false
    // Result: { color1: { name: 'Blue' }, color2: { name: 'Red' } }
  9. How the Mutative workflow works

    main

    Mutative follows a three-stage process to perform immutable updates using a mutable interface:

    1. Current State: The initial, unchanged immutable state.
    2. Draft: A mutable phase where changes are made to a draft. When you access properties of the state, Mutative generates corresponding draft proxy objects for those nodes.
    3. Next State: The final immutable data produced after the draft function completes. Only the parts of the state tree that were actually modified (or had drafts generated for them) are updated; unchanged branches are preserved via structural sharing.

    This allows you to write code that looks like direct mutation while maintaining strict immutability for the resulting state.

    const baseState = {
      a0: {
        b0: {},
      },
      a1: {
        b1: {},
        b2: {
          c0: 0,
        },
      },
      a2: {},
    }
    
    const nextState = create(baseState, (draft) => {
      const { a0 } = draft;
      // If it is draftable, once it has been accessed, it will generate a corresponding draft.
      expect(isDraft(a0)).toBeTruthy();
      // each node is a draft, and the draft is a proxy object
      draft.a1.b2.c0 = 1;
    });
    
    expect(nextState).not.toBe(baseState);
    expect(nextState.a0).toBe(baseState.a0); // generated draft, but not changed
    expect(nextState.a2).toBe(baseState.a2); // no generated draft, not changed
    expect(nextState.a1).not.toBe(baseState.a1);
    expect(nextState.a1.b2).not.toBe(baseState.a1.b2);
    expect(nextState.a1.b2.c0).toBe(1);
  10. Compare Mutative and Immer features

    main

    Mutative provides several advanced features and configuration options that are not available in Immer. Key differences include:

    • Custom shallow copy: Supported in Mutative.
    • Strict mode: Supported in Mutative.
    • Data freezing control: Mutative does not freeze data by default, whereas Immer relies on auto-freeze. Mutative also supports complete freeze data.
    • Non-invasive marking: Supported in Mutative.
    • Configuration: Mutative supports non-global configuration.
    • Async support: Mutative supports async draft functions.
    • JSON Patch: Mutative is fully compatible with the JSON Patch spec.
    • Set methods: Mutative (v1.1.0+) supports new Set methods.
  11. How currying works in Mutative

    main

    Currying in Mutative is a technique used to create modular and reusable state manipulation functions. It transforms a function that takes multiple arguments into a sequence of functions, allowing for partial application of arguments.

    This approach provides several benefits for state management:

    • Modular State Management: Create granular, composable functions for specific state updates.
    • Flexibility: Functions can be partially applied and reused across different contexts.
    • Code Reusability: Adapt a base function for different use cases by applying arguments incrementally.
    • Draft Integration: Works seamlessly with Mutative's draft-based system to apply incremental changes predictably.