Skip to content

Commit 31d11c0

Browse files
aibayanyu20claude
andcommitted
fix(util): preserve source when removing nested values in set (react-component/util#807)
- copy the parent object / array before deleting with removeIfUndefined - skip deletion when the parent is missing or a primitive - add set tests for source immutability and primitive parents Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 6d18589 commit 31d11c0

2 files changed

Lines changed: 64 additions & 3 deletions

File tree

‎packages/util/src/utils/set.ts‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,18 @@ function internalSet<Entity = any, Output = Entity, Value = any>(
2222
clone = ({ ...entity } as unknown) as Output
2323

2424
// Delete prop if `removeIfUndefined` and value is undefined
25-
if (removeIfUndefined && value === undefined && restPath.length === 1)
26-
delete (clone as any)[path][restPath[0]]
27-
else
25+
if (removeIfUndefined && value === undefined && restPath.length === 1) {
26+
// Copy the parent before deleting so the source entity is never mutated
27+
const origin = (clone as any)[path]
28+
if (origin && typeof origin === 'object') {
29+
const child = Array.isArray(origin) ? [...origin] : { ...origin }
30+
delete child[restPath[0] as any]
31+
;(clone as any)[path] = child
32+
}
33+
}
34+
else {
2835
(clone as any)[path] = internalSet((clone as any)[path], restPath, value, removeIfUndefined)
36+
}
2937

3038
return clone
3139
}

‎packages/util/tests/set.spec.ts‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { describe, expect, it } from 'vitest'
2+
import set from '../src/utils/set'
3+
4+
describe('set', () => {
5+
it('remove if undefined', () => {
6+
// Skip not exist path
7+
expect(set({}, ['notExist'], undefined, true)).toEqual({})
8+
9+
// Delete value
10+
const source = { keep: { light: 2333, bamboo: 1 } }
11+
const target = set(source, ['keep', 'light'], undefined, true)
12+
expect(target).toEqual({ keep: { bamboo: 1 } })
13+
expect('light' in target.keep).toBeFalsy()
14+
expect(source).toEqual({ keep: { light: 2333, bamboo: 1 } })
15+
expect(target.keep).not.toBe(source.keep)
16+
17+
// Delete array item without touching the source array
18+
const listSource = { list: [0, 1, 2] }
19+
const listTarget = set(listSource, ['list', 1], undefined, true)
20+
expect(1 in listTarget.list).toBeFalsy()
21+
expect(listSource.list).toEqual([0, 1, 2])
22+
23+
// Mid path not exist
24+
const midTgt = set({ lv1: { lv2: {} } }, ['lv1', 'lv2', 'lv3'], undefined, true)
25+
expect(midTgt).toEqual({ lv1: { lv2: {} } })
26+
expect('lv3' in midTgt.lv1.lv2).toBeFalsy()
27+
28+
// Long path not exist
29+
const longNotExistTgt = set({ lv1: { lv2: {} } }, ['lv1', 'lv2', 'lv3', 'lv4'], undefined, true)
30+
expect(longNotExistTgt).toEqual({ lv1: { lv2: {} } })
31+
expect('lv3' in longNotExistTgt.lv1.lv2).toBeFalsy()
32+
33+
// Long path remove
34+
const longTgt = set(
35+
{ lv1: { lv2: { lv3: { lv4: { lv: 5 } } } } },
36+
['lv1', 'lv2', 'lv3', 'lv4'],
37+
undefined,
38+
true,
39+
)
40+
expect(longTgt).toEqual({ lv1: { lv2: { lv3: {} } } })
41+
expect('lv4' in longTgt.lv1.lv2.lv3).toBeFalsy()
42+
})
43+
44+
it.each(['str', 123, true, Symbol('value'), BigInt(1)])(
45+
'preserves a primitive parent when removing a missing property: %s',
46+
(value) => {
47+
const source = { keep: value }
48+
const target = set(source, ['keep', 'light'], undefined, true)
49+
expect(target.keep).toBe(value)
50+
expect(source.keep).toBe(value)
51+
},
52+
)
53+
})

0 commit comments

Comments
 (0)