Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to deep clone ignoring immer references? (deepCurrent vs currrent) #1091

Closed
lveillard opened this issue Feb 18, 2024 · 4 comments
Closed

Comments

@lveillard
Copy link

lveillard commented Feb 18, 2024

Hello!

Issue
I'm struggling to mutate an object as i need to split it and then add different things to each new end. The problem is that the original object has nested objects, so when duplicating, the original nested objects get referenced.

Lets say i have

const a = {b: 0}

And then i split a to:

a => [a1, a2]
a1 => {b:0}
a2 => {b:0}

if i do this in the same draft and i modify b:2 it affects both.

I'm trying to sue structuredClone, structuredClone(current(a)) but they don't work because the draft are embeded.

With some real data:

subNode {
      '$thing': 'User',
      '$thingType': 'entity',
      '$op': 'update',
      '$bzId': 'R_9b59cc31-9df8-44ad-9dc2-839108f3f116',
      '$entity': 'User',
      '$id': <ref *1> [....} ],
      spaces: <ref *2> [
        {
          type_: 1,
          scope_: [Object],
          modified_: false,
          finalized_: false,
          assigned_: {},
          parent_: [Object],
          base_: [Array],
          draft_: [Circular *2],
          copy_: null,
          revoke_: [Function (anonymous)],
          isManual_: false
        }
      ]
    }

Basically I split the original object which has $id: ['id1' ,'id2'] into an array with two objects.

	return subNode.$id.map(($id: string, i: number) => ({
      ...subNode,
      $id: $id,
      $bzId: `${subNode.$bzId}_${i}`,
   }));

This works at the root node, i get an array with two different BzId "_0" and "_1"
But if i modify the nested object, it gets modified in both places.

I tried to use this: ...structuredClone(subNode),

But it gets this error: [object Array] could not be cloned.
I guess this happens when it arrives to the nested structures created by immer.

Is there a way in immer to deeply clone an object and drop?

or the only way i can do right is a deepCurrent() and then use structureCloned over the result?

Immer version:
Using immer (9.0.21 as 10.0.x does not work with symbols that have references to objects)

@lveillard lveillard changed the title How to deep clone ignoring immer references? How to deep clone ignoring immer references? (deepCurrent vs currrent) Feb 18, 2024
@mweststrate
Copy link
Collaborator

This might be a bug in current, but we need a repro to be sure. (See also comments in the PR)

@mweststrate
Copy link
Collaborator

#1105 should fix the handling of Proxy properties. When drafted, they are now finalized correctly. Let me know if it isn't solved by [email protected]

@lveillard
Copy link
Author

lveillard commented Mar 12, 2024

Well while the issue with the nested objects in Symbols is fixed, I still need to use my version of deepCurrent, i guess it is because I check extra stuff:

type Drafted<T> = T | Draft<T>;

// Recursively define the type to handle nested structures
type DeepCurrent<T> =
	T extends Array<infer U> ? Array<DeepCurrent<U>> : T extends object ? { [K in keyof T]: DeepCurrent<T[K]> } : T;

export const deepCurrent = <T>(obj: Drafted<T>): any => {
	if (Array.isArray(obj)) {
		// Explicitly cast the return type for arrays
		return obj.map((item) => current(item)) as DeepCurrent<T>;
	} else if (obj && typeof obj === 'object') {
		// Handle non-null objects
		const plainObject = isDraft(obj) ? current(obj) : obj;
		const result: any = {};
		Object.entries(plainObject).forEach(([key, value]) => {
			// Use the key to dynamically assign the converted value
			result[key] = current(value);
		});
		// Explicitly cast the return type for objects
		return result as DeepCurrent<T>;
	} else {
		// Return the value directly for non-objects and non-arrays
		return obj as DeepCurrent<T>;
	}
};

@lveillard
Copy link
Author

Well it actually works, I don't need it to be recursive anymore. I will change its name to "saferCurrent" tho as it does not throw error if the object is not a draft

type Drafted<T> = T | Draft<T>;

// Recursively define the type to handle nested structures
type DeepCurrent<T> =
	T extends Array<infer U> ? Array<DeepCurrent<U>> : T extends object ? { [K in keyof T]: DeepCurrent<T[K]> } : T;

export const deepCurrent = <T>(obj: Drafted<T>): any => {
	if (Array.isArray(obj)) {
		// Explicitly cast the return type for arrays
		return obj.map((item) => current(item)) as DeepCurrent<T>;
	} else if (obj && typeof obj === 'object') {
		// Handle non-null objects
		const plainObject = isDraft(obj) ? current(obj) : obj;
		const result: any = {};
		Object.entries(plainObject).forEach(([key, value]) => {
			// Use the key to dynamically assign the converted value
			result[key] = isDraft(value) ? current(value) : value;
		});
		// Explicitly cast the return type for objects
		return result as DeepCurrent<T>;
	} else {
		// Return the value directly for non-objects and non-arrays
		return obj as DeepCurrent<T>;
	}
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants