Skip to content

Commit a1959f5

Browse files
authored
fix(utils): updated deepClone to handle proxyMap and proxySet (#1074)
* updated deepClone to handle proxyMap and proxySet * ran eslint fix
1 parent 73655d1 commit a1959f5

File tree

4 files changed

+229
-0
lines changed

4 files changed

+229
-0
lines changed

src/vanilla/utils/deepClone.ts

+13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { unstable_getInternalStates } from '../../vanilla.ts'
2+
import { isProxyMap, proxyMap } from './proxyMap.ts'
3+
import { isProxySet, proxySet } from './proxySet.ts'
24

35
const isObject = (x: unknown): x is object =>
46
typeof x === 'object' && x !== null
@@ -18,6 +20,17 @@ export function deepClone<T>(
1820
if (!isObject(obj) || getRefSet().has(obj)) {
1921
return obj
2022
}
23+
24+
if (isProxySet(obj)) {
25+
return proxySet([...(obj as unknown as Iterable<unknown>)]) as unknown as T
26+
}
27+
28+
if (isProxyMap(obj)) {
29+
return proxyMap([
30+
...(obj as unknown as Map<unknown, unknown>).entries(),
31+
]) as unknown as T
32+
}
33+
2134
const baseObject: T = Array.isArray(obj)
2235
? []
2336
: Object.create(Object.getPrototypeOf(obj))

src/vanilla/utils/proxyMap.ts

+8
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ type InternalProxyObject<K, V> = Map<K, V> & {
1010
toJSON: () => Map<K, V>
1111
}
1212

13+
export const isProxyMap = (obj: object): boolean => {
14+
return (
15+
Symbol.toStringTag in obj &&
16+
obj[Symbol.toStringTag] === 'Map' &&
17+
proxyStateMap.has(obj)
18+
)
19+
}
20+
1321
/**
1422
* proxyMap
1523
*

src/vanilla/utils/proxySet.ts

+8
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ type InternalProxySet<T> = Set<T> & {
1818
isDisjointFrom: (other: Set<T>) => boolean
1919
}
2020

21+
export const isProxySet = (obj: object): boolean => {
22+
return (
23+
Symbol.toStringTag in obj &&
24+
obj[Symbol.toStringTag] === 'Set' &&
25+
proxyStateMap.has(obj)
26+
)
27+
}
28+
2129
/**
2230
* proxySet
2331
*

tests/deepClone.test.tsx

+200
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
import { proxy } from 'valtio'
3+
import { deepClone, proxyMap, proxySet } from 'valtio/utils'
4+
5+
describe('deepClone', () => {
6+
// Basic data types
7+
it('should handle primitive values', () => {
8+
expect(deepClone(42)).toBe(42)
9+
expect(deepClone('hello')).toBe('hello')
10+
expect(deepClone(true)).toBe(true)
11+
expect(deepClone(null)).toBe(null)
12+
expect(deepClone(undefined)).toBe(undefined)
13+
})
14+
15+
it('should clone plain objects', () => {
16+
const original = { a: 1, b: 'string', c: true }
17+
const cloned = deepClone(original)
18+
19+
expect(cloned).toEqual(original)
20+
expect(cloned).not.toBe(original) // Different reference
21+
})
22+
23+
it('should clone nested objects', () => {
24+
const original = {
25+
a: 1,
26+
b: {
27+
c: 'string',
28+
d: {
29+
e: true,
30+
},
31+
},
32+
}
33+
const cloned = deepClone(original)
34+
35+
expect(cloned).toEqual(original)
36+
expect(cloned.b).not.toBe(original.b) // Different reference for nested objects
37+
expect(cloned.b.d).not.toBe(original.b.d)
38+
})
39+
40+
it('should clone arrays', () => {
41+
const original = [1, 2, [3, 4, [5, 6]]]
42+
const cloned = deepClone(original)
43+
44+
expect(cloned).toEqual(original)
45+
expect(cloned).not.toBe(original)
46+
expect(cloned[2]).not.toBe(original[2])
47+
})
48+
49+
// Valtio specific tests
50+
it('should clone proxy objects', () => {
51+
const original = proxy({ a: 1, b: 2 })
52+
const cloned = deepClone(original)
53+
54+
expect(cloned).toEqual(original)
55+
expect(cloned).not.toBe(original)
56+
})
57+
58+
// ProxySet tests
59+
it('should properly clone a proxySet', () => {
60+
const original = proxySet<number>([1, 2, 3])
61+
const cloned = deepClone(original)
62+
63+
// Check if values are the same
64+
expect([...cloned]).toEqual([...original])
65+
66+
// Check if it's a different instance
67+
expect(cloned).not.toBe(original)
68+
69+
// Check if it's still a proxySet (by checking methods)
70+
expect(typeof cloned.add).toBe('function')
71+
expect(typeof cloned.delete).toBe('function')
72+
expect(typeof cloned.clear).toBe('function')
73+
expect(typeof cloned[Symbol.iterator]).toBe('function')
74+
expect(Object.prototype.toString.call(cloned)).toBe('[object Set]')
75+
})
76+
77+
it('should maintain proxySet reactivity', () => {
78+
const state = proxy({
79+
count: 0,
80+
set: proxySet<number>([1, 2, 3]),
81+
})
82+
83+
const cloned = deepClone(state)
84+
85+
// Add a new item to the cloned set
86+
cloned.set.add(4)
87+
88+
// Verify the item was added
89+
expect([...cloned.set]).toContain(4)
90+
91+
// Verify it's still a reactive proxySet (we can check by seeing if add method throws an error)
92+
expect(() => cloned.set.add(5)).not.toThrow()
93+
})
94+
95+
// ProxyMap tests
96+
it('should properly clone a proxyMap', () => {
97+
const original = proxyMap<string, number>([
98+
['a', 1],
99+
['b', 2],
100+
['c', 3],
101+
])
102+
const cloned = deepClone(original)
103+
104+
// Check if values are the same
105+
expect([...cloned.entries()]).toEqual([...original.entries()])
106+
107+
// Check if it's a different instance
108+
expect(cloned).not.toBe(original)
109+
110+
// Check if it's still a proxyMap (by checking methods)
111+
expect(typeof cloned.set).toBe('function')
112+
expect(typeof cloned.get).toBe('function')
113+
expect(typeof cloned.delete).toBe('function')
114+
expect(typeof cloned.clear).toBe('function')
115+
expect(typeof cloned.entries).toBe('function')
116+
expect(Object.prototype.toString.call(cloned)).toBe('[object Map]')
117+
})
118+
119+
it('should maintain proxyMap reactivity', () => {
120+
const state = proxy({
121+
count: 0,
122+
map: proxyMap<string, number>([
123+
['a', 1],
124+
['b', 2],
125+
]),
126+
})
127+
128+
const cloned = deepClone(state)
129+
130+
// Set a new entry in the cloned map
131+
cloned.map.set('c', 3)
132+
133+
// Verify the entry was added
134+
expect(cloned.map.get('c')).toBe(3)
135+
136+
// Verify it's still a reactive proxyMap (we can check by seeing if set method throws an error)
137+
expect(() => cloned.map.set('d', 4)).not.toThrow()
138+
})
139+
140+
// Complex object with both proxySet and proxyMap
141+
it('should handle complex objects with both proxySet and proxyMap', () => {
142+
const original = proxy({
143+
name: 'test',
144+
count: 42,
145+
set: proxySet<number>([1, 2, 3]),
146+
map: proxyMap<string, any>([
147+
['a', 1],
148+
['b', { nested: true }],
149+
['c', proxySet<string>(['x', 'y', 'z'])],
150+
]),
151+
nested: {
152+
anotherSet: proxySet<string>(['a', 'b', 'c']),
153+
},
154+
})
155+
156+
const cloned = deepClone(original)
157+
158+
// Check basic properties
159+
expect(cloned.name).toBe('test')
160+
expect(cloned.count).toBe(42)
161+
162+
// Check proxySet
163+
expect([...cloned.set]).toEqual([1, 2, 3])
164+
165+
// Check proxyMap
166+
expect(cloned.map.get('a')).toBe(1)
167+
expect(cloned.map.get('b')).toEqual({ nested: true })
168+
169+
// Check nested proxySet inside proxyMap
170+
const nestedSet = cloned.map.get('c')
171+
expect([...nestedSet]).toEqual(['x', 'y', 'z'])
172+
expect(typeof nestedSet.add).toBe('function')
173+
174+
// Check nested object with proxySet
175+
expect([...cloned.nested.anotherSet]).toEqual(['a', 'b', 'c'])
176+
177+
// Verify reactivity is maintained
178+
expect(() => cloned.set.add(4)).not.toThrow()
179+
expect(() => cloned.map.set('d', 4)).not.toThrow()
180+
expect(() => cloned.map.get('c').add('w')).not.toThrow()
181+
expect(() => cloned.nested.anotherSet.add('d')).not.toThrow()
182+
})
183+
184+
// Edge cases
185+
it('should handle empty proxySet and proxyMap', () => {
186+
const original = proxy({
187+
emptySet: proxySet<number>(),
188+
emptyMap: proxyMap<string, number>(),
189+
})
190+
191+
const cloned = deepClone(original)
192+
193+
expect(cloned.emptySet.size).toBe(0)
194+
expect(cloned.emptyMap.size).toBe(0)
195+
196+
// Verify they're still proxy collections
197+
expect(() => cloned.emptySet.add(1)).not.toThrow()
198+
expect(() => cloned.emptyMap.set('a', 1)).not.toThrow()
199+
})
200+
})

0 commit comments

Comments
 (0)