Immer: Data not originating from the state is being drafted

81 Views Asked by At

According to the documentation for Immer, data not originating from the state will not be drafted(immer - pitfalls). However, I encountered an issue where it seems like such data is being drafted contrary to the documentation.

Here's an example:

import produce from "immer";

let baseState = {
  value: { x: 1 },
  t: "a",
};

let someVal = { x: 2 };
let nextState = produce(baseState, (draft) => {
  draft.value = someVal;
  draft.value.x = 1; // Results in false
});

console.log("baseState", JSON.stringify(baseState));
console.log("nextState", JSON.stringify(nextState));
console.log("isEqual", baseState === nextState);

As per my understanding, drafting implies copying the data to a temporary place, so the baseState and nextState values should be equal in this scenario. However, the comparison at the end (baseState === nextState) results in false.

Why baseState and nextState are not equal in this context despite the documentation suggesting that data not originating from the state should not be drafted?

1

There are 1 best solutions below

2
On

Object variables in Javascript hold a reference to the value of the object.

This means if you have an object variable const objectA = { propertyA: 1}; and you create a new variable const objectB = objectA; both variables contain the same reference to the value of the object.

What this means is that if you mutate the object referenced by objectA you are also mutating the object referenced by objectB.

// Create our initial object value and assign the reference to
// the variable objectA.
let objectA = {
    propertyA: 1
}

// Assign the same reference to the object value created with 
// the previous statement to objectB.
let objectB = objectA;

console.log('Initial Objects:')
console.log('objectA:', objectA);
console.log('objectB:', objectB);
// This is a reference comparison as long as both variables 
// contian the same reference this comparison will be true
console.log('Match:', objectA === objectB); 

console.log('')

// Add a new property to the object value reference by
// objectA, this will also be reflected in objectB as
// both variables still store the same reference.
objectA.propertyB = [ 2 ];

console.log('After Mutation:')
console.log('objectA:', objectA);
console.log('objectB:', objectB);
// This is a reference comparison as long as both variables 
// contian the same reference this comparison will be true.
// As the references haven't changed, it's still true.
console.log('Match:', objectA === objectB);
// Arrays also are treated similar to objects and any variable
// or property that points to one will be storing a reference
// to the value.
// As objectA and objectB reference the same object value
// objectA.propertyB, objectB.propertyB will be the same reference.
console.log('Match propertyB:', objectA.propertyB === objectB.propertyB);

console.log('')

// Reassign objectB to reference a new object that
// is a shallow copy of the object value referenced
// by objectA. (uses the spread operator to achieve
// the shallow copy)
objectB = {...objectA};

console.log('Reassign objectB:')
console.log('objectA:', objectA);
console.log('objectB:', objectB);
// This is a reference comparison as long as both variables 
// contian the same reference this comparison will be true.
// As objectB now references a different object value it's 
// no longer true.
console.log('Match:', objectA === objectB);
// Arrays also are treated similar to objects and any variable
// or property that points to one will be storing a reference
// to the value.
// As we only shallow copied objectA to objectB the reference
// stored at objectB.propertyB will still be the same as the
// one stored at objectA.propertyB.
console.log('Match propertyB:', objectA.propertyB === objectB.propertyB);

The limitation you have referenced in Immer is talking about it's inability to automatically draft random values you throw at it on the fly in a produce function.

What does that mean?!

The last time I looked Immer was using a Proxy (or their own implementation of one) to track changes in the produce function.

When you assign an object reference (someVal) to a property on the draft the proxy is recording the change you to want make and the property path you wish to make it to.

Immer is only recording this as a change of reference, it has recorded nothing (NOTHING) about the value that is being referenced.

When the produce function completes Immer will run through and create a new object with the changes that were recorded.

It will see the reference change only, it will know nothing of the changes made to the value that is being referenced in between it being recorded and the state being updated.

let baseState = {
  // with value { x: 1 }
  value: { x: 1 }, 
  t: "a",
};

let someVal = { x: 2 };

let nextState = produce(baseState, (draft) => {
    // Assign the reference stored by someVal to draft.value
    // In the produce function this will trigger the proxy to
    // record a reference change to the 'value' property path.
    draft.value = someVal;

    // The below line is modifying the object referenced by
    // draft.value (also someVal).
    // As this object is NOT drafted it will be modified
    // directly instead of changes being recorded and
    // applied later.
    // This can be seen by checking the value of someVal
    // in the produce function immediately after this
    // change.
    draft.value.x = 1;

    // someVal.x will now be 1.
    console.log(someVal)
});

Also console.log("isEqual", baseState === nextState); should return false when any change is made to the draft during the produce function.

The point of immer is to turn those changes into a new immutable object, this new immutable object will have a different reference to the original ( === between objects is a reference comparison not a value comparison).