Skip to content

Commit cdccf1a

Browse files
authored
Merge pull request #1210 from immerjs/bugfix/1209-array-plugin-nested-drafts
fix: handle nested proxies after spreading and inserting into an array
2 parents 570c800 + 90a7765 commit cdccf1a

File tree

2 files changed

+92
-0
lines changed

2 files changed

+92
-0
lines changed

__tests__/base.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2778,6 +2778,53 @@ function runBaseTest(
27782778
expect(nextState).toEqual({foo: {bar: {a: true, c: true}}})
27792779
})
27802780

2781+
// #1209 - spread draft object and push back via array methods
2782+
it("can spread a draft and push it back into the array", () => {
2783+
const base = [{nestedArray: []}]
2784+
const next = produce(base, s => {
2785+
s.push({...s[0]})
2786+
})
2787+
expect(next[0].nestedArray).toEqual([])
2788+
expect(next[1].nestedArray).toEqual([])
2789+
expect(next[1].nestedArray.length).toBe(0)
2790+
})
2791+
2792+
it("can spread a draft with nested objects and push it back", () => {
2793+
const base = [{nested: {value: 42}}]
2794+
const next = produce(base, s => {
2795+
s.push({...s[0]})
2796+
})
2797+
expect(next[0].nested.value).toBe(42)
2798+
expect(next[1].nested.value).toBe(42)
2799+
})
2800+
2801+
it("can push a draft value directly into its parent array", () => {
2802+
const base = [{nestedArray: []}]
2803+
const next = produce(base, s => {
2804+
s.push(s[0])
2805+
})
2806+
expect(next[0].nestedArray).toEqual([])
2807+
expect(next[1].nestedArray).toEqual([])
2808+
})
2809+
2810+
it("can unshift a spread draft back into the array", () => {
2811+
const base = [{nestedArray: [1]}]
2812+
const next = produce(base, s => {
2813+
s.unshift({...s[0]})
2814+
})
2815+
expect(next[0].nestedArray).toEqual([1])
2816+
expect(next[1].nestedArray).toEqual([1])
2817+
})
2818+
2819+
it("can splice a spread draft into the array", () => {
2820+
const base = [{nestedArray: ["a", "b"]}]
2821+
const next = produce(base, s => {
2822+
s.splice(0, 0, {...s[0]})
2823+
})
2824+
expect(next[0].nestedArray).toEqual(["a", "b"])
2825+
expect(next[1].nestedArray).toEqual(["a", "b"])
2826+
})
2827+
27812828
it("supports assigning undefined to an existing property", () => {
27822829
const nextState = produce(baseState, s => {
27832830
s.aProp = undefined

src/plugins/arrayMethods.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
loadPlugin,
55
markChanged,
66
prepareCopy,
7+
handleCrossReference,
78
ProxyArrayState
89
} from "../internal"
910

@@ -189,6 +190,30 @@ export function enableArrayMethods() {
189190
return Math.min(index, length)
190191
}
191192

193+
/**
194+
* Calls handleCrossReference for each value being inserted into the array,
195+
* and marks the corresponding indices as assigned in `assigned_`.
196+
*
197+
* This ensures nested drafts inside inserted values (e.g. from spreading
198+
* a draft object) are properly finalized, matching the behavior of the
199+
* proxy set trap which calls handleCrossReference on every assignment.
200+
*
201+
* Without this, values containing draft proxies (like `{...state[0]}`)
202+
* pushed via the array methods plugin would have their nested drafts
203+
* revoked during finalization without being replaced by final values.
204+
*/
205+
function handleInsertedValues(
206+
state: ProxyArrayState,
207+
startIndex: number,
208+
values: any[]
209+
) {
210+
for (let i = 0; i < values.length; i++) {
211+
const index = startIndex + i
212+
state.assigned_!.set(index, true)
213+
handleCrossReference(state, index, values[i])
214+
}
215+
}
216+
192217
/**
193218
* Handles mutating operations that add/remove elements (push, pop, shift, unshift, splice).
194219
*
@@ -204,13 +229,25 @@ export function enableArrayMethods() {
204229
args: any[]
205230
) {
206231
return executeArrayMethod(state, () => {
232+
// For push/unshift, capture the length before the operation
233+
// so we can compute insertion indices for handleCrossReference
234+
const lengthBefore = state.copy_!.length
235+
207236
const result = (state.copy_! as any)[method](...args)
208237

209238
// Handle index reassignment for shifting methods
210239
if (SHIFTING_METHODS.has(method as MutatingArrayMethod)) {
211240
markAllIndicesReassigned(state)
212241
}
213242

243+
// Handle cross-references for newly inserted values.
244+
// push appends at the end, unshift inserts at the beginning.
245+
if (method === "push" && args.length > 0) {
246+
handleInsertedValues(state, lengthBefore, args)
247+
} else if (method === "unshift" && args.length > 0) {
248+
handleInsertedValues(state, 0, args)
249+
}
250+
214251
// Return appropriate value based on method
215252
return RESULT_RETURNING_METHODS.has(method as MutatingArrayMethod)
216253
? result
@@ -285,6 +322,14 @@ export function enableArrayMethods() {
285322
state.copy_!.splice(...(args as [number, number, ...any[]]))
286323
)
287324
markAllIndicesReassigned(state)
325+
// Handle cross-references for inserted values (args from index 2+)
326+
if (args.length > 2) {
327+
const startIndex = normalizeSliceIndex(
328+
args[0] ?? 0,
329+
state.copy_!.length
330+
)
331+
handleInsertedValues(state, startIndex, args.slice(2))
332+
}
288333
return res
289334
}
290335
} else {

0 commit comments

Comments
 (0)