Skip to content

Nested create() on draft element shares references with original base object #160

@knutwannheden

Description

@knutwannheden

Description

When calling create() on an object that is already a Mutative draft proxy, the resulting object's nested properties that weren't modified in the recipe share references with the original base object, not the draft's current state.

This allows accidental mutation of the original object, which violates immutability expectations.

Minimal Reproduction

import { create } from 'mutative';

interface Metadata {
    value: string;
}

interface Node {
    name: string;
    metadata: Metadata;
}

interface Tree {
    nodes: Node[];
}

// Helper function that uses create() internally - common pattern for reusable transformations
function updateName(node: Node, newName: string): Node {
    return create(node, (draft) => {
        draft.name = newName;
    });
}

const original: Tree = {
    nodes: [
        { name: 'a', metadata: { value: '' } }
    ]
};

const result = create(original, (draft) => {
    draft.nodes = draft.nodes.map((node) => {
        // Calling a helper that uses create() - doesn't look like nested create()
        const updated = updateName(node, 'modified');

        // BUG: updated.metadata shares reference with original
        updated.metadata.value = 'oops';
        return updated;
    });
});

console.log('Original mutated?', original.nodes[0].metadata.value === 'oops'); // true!

Note: In real code, the nested create() call is typically hidden inside a helper function (like updateName above), making it non-obvious that you're calling create() on a draft element.

Expected Behavior

When create() is called on a draft element, the resulting object should not share mutable references with the original base object. Either:

  1. Nested create() should work correctly (like Immer handles nested produce())
  2. Or an error/warning should be thrown indicating this pattern isn't supported

Actual Behavior

The object returned from the inner create() call has nested properties (metadata) that reference the original base object. Mutating these nested properties unexpectedly mutates the original.

Environment

  • mutative version: latest
  • Node.js version: v20+

Context

We discovered this while migrating from Immer to Mutative. Our codebase uses a visitor pattern where helper functions call create() to modify parts of a tree, and these helpers are sometimes invoked from within a create() recipe. This pattern worked correctly with Immer but causes original tree mutation with Mutative.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions