0

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?

5
  • Put a console.log(`Cloning ${prop}`); in the line before { ...target[prop] } and you'll see what's happening Commented May 3, 2024 at 7:58
  • Bergi, could you please elaborate what I should see there? I see only "Cloning x" because x is the only property to clone there. Commented May 3, 2024 at 9:29
  • Yes, but how many times would you expect to see it? Commented May 3, 2024 at 10:07
  • Well...in case of proxy8.x.y = proxy8.x.y + 2; I expect to see it 2 times and I see it 2 times. I case of const t = proxy8.x.y + 2; proxy8.x.y = t; I see it 2 times as well. This looks normal to me. Commented May 3, 2024 at 10:59
  • Oh right it's a bit more intricate to log this than I thought at first. I had to hand out object ids in my answer, but those hopefully make it clear… Commented May 3, 2024 at 11:42

1 Answer 1

0

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.

Sign up to request clarification or add additional context in comments.

2 Comments

Thank you, but in your code, obj.x.y = obj.x.y + 2; still doesn't update the "y" property. After the first "Done" it is still { x: { y: 5, id: 2 }, id: 'root' }.
Yes, this answer only offers an explanation of what's happening, it's still the same code as yours just with more logging.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.