How to make a structuredClone of a Proxy object?
Asked Answered
I

3

30

I'm using Vue3 where a lot of the objects are Proxy objects for reactivity. I want to create a deep copy of one of the proxy objects and recently discovered structuredClone.

https://developer.mozilla.org/en-US/docs/Web/API/structuredClone

When I run the following code, I get an error performing a structuredClone on proxyObj:

const obj = {
    name: "Steve",
    age: 50
}
const handler = {}
const proxyObj = new Proxy(obj, {})

console.log(proxyObj)

const objCopy = structuredClone(obj)

console.log(objCopy)

const proxyObjCopy = structuredClone(proxyObj)
console.log(objCopy)

Uncaught DOMException: Failed to execute 'structuredClone' on 'Window': # could not be cloned.

Is there a way I can clone the proxy object? Is there a way I can dereference it first, copy it, and not lose the reactivity? Any help is appreciated!

Indiaindiaman answered 11/2, 2022 at 4:51 Comment(9)
To be clear, you are not responsible for the creation of the Proxy and have no access to the target object, right? Otherwise #44119339 could have helped.Footman
Correct, the proxy creation is done behind the scenes as far as I can tellIndiaindiaman
And would a simple structuredClone({...proxyObj}) get you all the expected key-value pairs or is your the object more complex than the example you gave?Footman
That does work and is nicer than the Object.assign implementation I found. The only issue is that it strips the proxy. I'll have to check for my use case if I need the reactivity after I do this assignment.Indiaindiaman
Proxy is not supported, end of the story developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/… . Any way, it's unlikely a reactive object would work as intended this way, I'd expect it to have internal state. You need to stick to composition API to compose objects and maintain reactivityQuits
Can you elaborate on stick to composition API? The way I'm handling it is using @kaiido 's solution. When I return from the copy with the stripped proxy, I map the properties of the result to the property in state that has the proxy. It's a bit hacky, but looks like that'll have to do since Proxy copies aren't supported. Thank youIndiaindiaman
This is how this can be done with Vue proxies, #72632673 . This doesn't apply to the code in the question because it uses raw proxies which cannot use the same approach.Quits
@Footman I get this error Uncaught TypeError: Spread syntax requires ...iterable[Symbol.iterator] to be a functionFleta
structuredClone returns a clone of the structure. It does loose reactivity by definition.Advisable
F
16

If you're just talking about Vue3 and no nested proxies you could just use toRaw directly:

import { reactive, toRaw } from 'vue';

const obj = reactive({ foo: 'bar' });

const objCopy = structuredClone(toRaw(obj)):

Having come across a few cases where I had objects with nested proxies or arrays of proxies etc, I had to go the extra mile and create my own toRawDeep.

Since it uses toRaw it should work with proxies created by reactive(), readonly(), shallowReactive() or shallowReadonly().

import { toRaw } from 'vue';

export function toRawDeep<T>(observed: T): T {
    const val = toRaw(observed);

    if (Array.isArray(val)) {
        return val.map(toRawDeep) as T;
    }

    if (val === null) return null as T;

    if (typeof val === 'object') {
        const entries = Object.entries(val).map(([key, val]) => [key, toRawDeep(val)]);

        return Object.fromEntries(entries);
    }

    return val;
}
Fete answered 1/9, 2023 at 9:35 Comment(0)
F
4

A simple way is to to convert Proxy object to standard object. Check this answer : https://mcmap.net/q/500962/-how-can-i-reliably-convert-a-proxy-to-a-standard-object

In the case you presented :

const obj = {
    name: "John",
    age: 50,
    children: [{
        name: 'Sam'
    }]
};

const proxyObj = new Proxy(obj, {});
const objOneLevelCopy = {...proxyObj};
// const objCopy = structuredClone(proxyObj) // throw Error
const objStructuredCopy = structuredClone({...proxyObj});

obj.name = 'Amanda' // change a value
obj.children[0].name = 'Mike'; // change a sub value

console.log({obj}) // { name: "Amanda", age: 50, children: [{name: 'Mike'}] }
console.log({proxyObj}) // { name: "Amanda", age: 50, children: [{name: 'Mike'}] }
console.log({objOneLevelCopy}) // { name: "John", age: 50, children: [{name: 'Mike'}] }
console.log({objStructuredCopy}); // { name: "John", age: 50, children: [{name: 'Sam'}] }
Freeway answered 12/1 at 18:33 Comment(0)
F
-1

Following your example you can create your own function using Object.create and Object.getOwnPropertyDescriptors

function cloneProxy(obj) {
    //creates a new object that inherits from the prototype of the original object
    const clone = Object.create(Object.getPrototypeOf(obj));
    const properties = Object.getOwnPropertyDescriptors(obj);
    
    for (prop in properties) {
        if (properties[prop].hasOwnProperty("value")) {
            clone[prop] = properties[prop].value;
        } else {
            clone[prop] = properties[prop];
        }
    }
    
    return clone;
}

const obj = {
    name: "Steve",
    age: 50
}
const handler = {}
const proxyObj = new Proxy(obj, {})
console.log(proxyObj) //{ name: 'Steve', age: 50 }

const objCopy = structuredClone(obj)
console.log(objCopy) //{ name: 'Steve', age: 50 }

const proxyObjCopy = cloneProxy(proxyObj)
console.log(proxyObjCopy) // { name: 'Steve', age: 50 }

For more info about these functions:

Object.create --> https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create

Object.getOwnPropertyDescriptors --> https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptors

Fleta answered 24/8, 2023 at 19:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.