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:
- Nested
create() should work correctly (like Immer handles nested produce())
- 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.
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
Note: In real code, the nested
create()call is typically hidden inside a helper function (likeupdateNameabove), making it non-obvious that you're callingcreate()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:create()should work correctly (like Immer handles nestedproduce())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
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 acreate()recipe. This pattern worked correctly with Immer but causes original tree mutation with Mutative.