immerjs / immer

Create the next immutable state by mutating the current one

Home Page:https://immerjs.github.io/immer/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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

lveillard opened this issue · comments

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)

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

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

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>;
	}
};

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>;
	}
};