Skip to content

Commit b3eeb69

Browse files
authoredJan 15, 2023
fix: Preserve insertion order of Sets, fixes #819 (#976)
* Proposed fix for #819 to preserve Set insertion order * Proposed fix for #819 (B) We are already cloning and iterating over the Set. If we go a step further and clear it before iteration, and re-add all children during iteration, we can preserve insertion order.
1 parent b9eae1d commit b3eeb69

File tree

3 files changed

+49
-8
lines changed

3 files changed

+49
-8
lines changed
 

‎__tests__/map-set.js

+34
Original file line numberDiff line numberDiff line change
@@ -296,4 +296,38 @@ function runBaseTest(name, useProxies, autoFreeze, useListener) {
296296
expect(mapType1).toBe(mapType2)
297297
})
298298
})
299+
describe("set issues " + name, () => {
300+
test("#819.A - maintains order when adding", () => {
301+
const objs = [
302+
"a",
303+
{
304+
id: "b"
305+
}
306+
]
307+
308+
const set = new Set([objs[0]])
309+
const newSet = produce(set, draft => {
310+
draft.add(objs[1])
311+
})
312+
313+
// passes
314+
expect(Array.from(newSet)).toEqual([objs[0], objs[1]])
315+
})
316+
317+
test("#819.B - maintains order when adding", () => {
318+
const objs = [
319+
{
320+
id: "a"
321+
},
322+
"b"
323+
]
324+
325+
const set = new Set([objs[0]])
326+
const newSet = produce(set, draft => {
327+
draft.add(objs[1])
328+
})
329+
330+
expect(Array.from(newSet)).toEqual([objs[0], objs[1]])
331+
})
332+
})
299333
}

‎src/core/finalize.ts

+15-7
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,17 @@ function finalize(rootScope: ImmerScope, value: any, path?: PatchPath) {
8787
: state.copy_
8888
// Finalize all children of the copy
8989
// For sets we clone before iterating, otherwise we can get in endless loop due to modifying during iteration, see #628
90-
// Although the original test case doesn't seem valid anyway, so if this in the way we can turn the next line
91-
// back to each(result, ....)
92-
each(
93-
state.type_ === ProxyType.Set ? new Set(result) : result,
94-
(key, childValue) =>
95-
finalizeProperty(rootScope, state, result, key, childValue, path)
90+
// To preserve insertion order in all cases we then clear the set
91+
// And we let finalizeProperty know it needs to re-add non-draft children back to the target
92+
let resultEach = result
93+
let isSet = false
94+
if (state.type_ === ProxyType.Set) {
95+
resultEach = new Set(result)
96+
result.clear()
97+
isSet = true
98+
}
99+
each(resultEach, (key, childValue) =>
100+
finalizeProperty(rootScope, state, result, key, childValue, path, isSet)
96101
)
97102
// everything inside is frozen, we can freeze here
98103
maybeFreeze(rootScope, result, false)
@@ -115,7 +120,8 @@ function finalizeProperty(
115120
targetObject: any,
116121
prop: string | number,
117122
childValue: any,
118-
rootPath?: PatchPath
123+
rootPath?: PatchPath,
124+
targetIsSet?: boolean
119125
) {
120126
if (__DEV__ && childValue === targetObject) die(5)
121127
if (isDraft(childValue)) {
@@ -134,6 +140,8 @@ function finalizeProperty(
134140
if (isDraft(res)) {
135141
rootScope.canAutoFreeze_ = false
136142
} else return
143+
} else if (targetIsSet) {
144+
targetObject.add(childValue)
137145
}
138146
// Search new objects for unfinalized drafts. Frozen objects should never contain drafts.
139147
if (isDraftable(childValue) && !isFrozen(childValue)) {

‎src/utils/common.ts

-1
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,6 @@ export function set(thing: any, propOrOldValue: PropertyKey, value: any) {
132132
const t = getArchtype(thing)
133133
if (t === Archtype.Map) thing.set(propOrOldValue, value)
134134
else if (t === Archtype.Set) {
135-
thing.delete(propOrOldValue)
136135
thing.add(value)
137136
} else thing[propOrOldValue] = value
138137
}

0 commit comments

Comments
 (0)
Please sign in to comment.