Black Beard
Black Beard

Reputation: 1618

Expression which contains deeply nested proxy object property does not update the same proxy object property

I have a function that takes an object and returns a proxied version of this object, which can be mutated without affecting the original object.

const createProxy4 = (obj) => {
  const handler = {
    get(target, prop) {
      //if the value of a property is an object - replace it with another proxy before return
      if (target[prop] !== null && typeof target[prop] === "object") {
        target[prop] = new Proxy({ ...target[prop] }, handler);
      }
      //if the value is a primitive - just return it
      return target[prop];
    },
    set(target, prop, value) {
      //to update the property - just assign a new value to it
      target[prop] = value;
      return true;
    },
  };

  //create a shallow copy of the original object
  return new Proxy({ ...obj }, handler);
};

Example

//here is an original object
const obj8 = {
  x: { y: 5 },
};
//let's create a proxied copy of it
const proxy8 = createProxy4(obj8);

//let's change the property of the proxied object
proxy8.x.y = 27;
console.log(obj8); //{ x: { y: 5 } } - original object is not affected
console.log(proxy8); //{ x: { y: 27 } } - proxied object property is changed as expected

//let's change proxy using expression  which uses it's own property
proxy8.x.y = proxy8.x.y + 2;
console.log(obj8); //{ x: { y: 5 } } - original object is not affected
console.log(proxy8); //{ x: { y: 27 } } - NOTHING CHANGED! WHY?

//let's refactor the previous expression using an intermediate variable "t"
const t = proxy8.x.y + 2;
proxy8.x.y = t;
console.log(obj8); //{ x: { y: 5 } } - original object is not affected
console.log(proxy8); //{ x: { y: 29 } } - Changes applied as expected. WHY? 

Question
So, from my point of view, expressions

proxy8.x.y = proxy8.x.y + 2;
//doesn't work

and

const t = proxy8.x.y + 2;
proxy8.x.y = t;
//works

are almost identical.
But why one of them doesn't work? Why does an expression that contains a deeply nested proxy object property not update the same proxy object property?

Upvotes: 0

Views: 47

Answers (1)

Bergi
Bergi

Reputation: 665276

The difference - and the problem - is the order of operations:

function createProxy(obj) {
  let id = 0;
  const handler = {
    get(target, prop, receiver) {
      let value = Reflect.get(target, prop, receiver);
      if (value !== null && typeof value === "object") {
        console.log(`Replacing ${prop} on ${target.id} with new object ${++id}`);
        value = target[prop] = new Proxy({ ...value, id }, handler);
      }
      return value;
    },
    set(target, prop, value, receiver) {
      console.log(`Assigning ${prop} on object ${target.id}`);
      return Reflect.set(target, prop, value, receiver);
    },
  };
  return new Proxy({ ...obj, id: 'root' }, handler);
};

const obj = createProxy({
  x: { y: 5 },
});


console.log('Single statement');
obj.x.y = obj.x.y + 2;
console.log('Done:');
console.log(obj);

console.log('Two statements');
const t = obj.x.y + 2;
obj.x.y = t;
console.log('Done:');
console.log(obj);

console.log('Compound assignment');
obj.x.y += 2;
console.log('Done:');
console.log(obj);

In the two statements

const t = obj.x.y + 2;
obj.x.y = t;

the access of .y is executed first, replacing obj.x with a new object in that step, followed by the assignment to the target .y that again replaces obj.x with a new object (on which the new y is then stored).

In contrast, in the single statement

obj.x.y = obj.x.y + 2;

the target reference is evaluated first, replacing obj.x in that step, followed by the access of y that again replaces obj.x with a new value, and in the end the new y is stored in the object resulting from the first step that is no longer the current value of obj.x.

To fix this, don't replace your proxy properties with new objects on every single access, but a) only if necessary when an assignment happens or b) only once per target.

Upvotes: 0

Related Questions