Skip to content

Commit 4e4048e

Browse files
committed
fix: reassigning to same value generates new copy with empty patches
1 parent cdccf1a commit 4e4048e

3 files changed

Lines changed: 143 additions & 22 deletions

File tree

__tests__/base.js

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ function runBaseTest(
179179
delete s.a
180180
s.a = a
181181
})
182-
expect(nextState).not.toBe(baseState)
182+
expect(nextState).toBe(baseState)
183183
expect(nextState).toEqual(baseState)
184184
})
185185

@@ -3274,8 +3274,7 @@ function runBaseTest(
32743274
draft.highlight = false
32753275
draft.highlight = true
32763276
})
3277-
// See explanation in issue
3278-
expect(next1).not.toBe(a)
3277+
expect(next1).toBe(a)
32793278

32803279
const next2 = produce(a, draft => {
32813280
draft.highlight = true
@@ -3496,7 +3495,7 @@ function runBaseTest(
34963495
expect(next).toEqual({dots: base.availableStartingDots})
34973496
})
34983497

3499-
it("cannot always detect noop assignments - 0", () => {
3498+
it("detects noop assignments - 0", () => {
35003499
const baseState = {x: {y: 3}}
35013500
const nextState = produce(baseState, d => {
35023501
const a = d.x
@@ -3505,54 +3504,45 @@ function runBaseTest(
35053504
expect(nextState).toBe(baseState)
35063505
})
35073506

3508-
it("cannot always detect noop assignments - 1", () => {
3507+
it("detects noop assignments - 1", () => {
35093508
const baseState = {x: {y: 3}}
35103509
const nextState = produce(baseState, d => {
35113510
const a = d.x
35123511
d.x = 4
35133512
d.x = a
35143513
})
3515-
// Ideally, this should actually be the same instances
3516-
// but this would be pretty expensive to detect,
3517-
// so we don't atm
3518-
expect(nextState).not.toBe(baseState)
3514+
expect(nextState).toBe(baseState)
35193515
})
35203516

3521-
it("cannot always detect noop assignments - 2", () => {
3517+
it("detects noop assignments - 2", () => {
35223518
const baseState = {x: {y: 3}}
35233519
const nextState = produce(baseState, d => {
35243520
const a = d.x
35253521
const stuff = a.y + 3
35263522
d.x = 4
35273523
d.x = a
35283524
})
3529-
// Ideally, this should actually be the same instances
3530-
// but this would be pretty expensive to detect,
3531-
// so we don't atm
3532-
expect(nextState).not.toBe(baseState)
3525+
expect(nextState).toBe(baseState)
35333526
})
35343527

3535-
it("cannot always detect noop assignments - 3", () => {
3528+
it("detects noop assignments - 3", () => {
35363529
const baseState = {x: 3}
35373530
const nextState = produce(baseState, d => {
35383531
d.x = 3
35393532
})
35403533
expect(nextState).toBe(baseState)
35413534
})
35423535

3543-
it("cannot always detect noop assignments - 4", () => {
3536+
it("detects noop assignments - 4", () => {
35443537
const baseState = {x: 3}
35453538
const nextState = produce(baseState, d => {
35463539
d.x = 4
35473540
d.x = 3
35483541
})
3549-
// Ideally, this should actually be the same instances
3550-
// but this would be pretty expensive to detect,
3551-
// so we don't atm
3552-
expect(nextState).not.toBe(baseState)
3542+
expect(nextState).toBe(baseState)
35533543
})
35543544

3555-
it("cannot always detect noop assignments - 4", () => {
3545+
it("cannot always detect noop assignments - 1", () => {
35563546
const baseState = {}
35573547
const [nextState, patches] = produceWithPatches(baseState, d => {
35583548
d.x = 4

__tests__/produce.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,95 @@ it("can apply readonly patches", () => {
177177
expect(applyPatches({}, patches)).toEqual({x: 4})
178178
})
179179

180+
describe("assigning a value and then reverting it, generates no changes", () => {
181+
it("top-level", () => {
182+
type Item = {date: Date}
183+
184+
let initialDate = new Date()
185+
let data: Item = {date: initialDate}
186+
187+
const [newData, patches] = produceWithPatches(data, draft => {
188+
const dateBefore = draft.date
189+
draft.date = new Date()
190+
draft.date = dateBefore
191+
})
192+
193+
// Expect no patches and the returned value to be the same reference
194+
expect(patches).toEqual([])
195+
expect(newData).toBe(data)
196+
})
197+
198+
it("top-level pathological case", () => {
199+
type Item = {number: Number}
200+
201+
let data: Item = {number: -0}
202+
203+
const [newData, patches] = produceWithPatches(data, draft => {
204+
draft.number = 1
205+
draft.number = +0
206+
})
207+
208+
// Expect no patches and the returned value to be the same reference
209+
expect(newData).not.toBe(data)
210+
// Bug in patches, should be a patch but none generated
211+
expect(patches).toEqual([])
212+
})
213+
214+
it("top-level with adjacent modified draft", () => {
215+
type Item = {date: Date; other: {x: number}}
216+
217+
let initialDate = new Date()
218+
let data: Item = {date: initialDate, other: {x: 0}}
219+
220+
const [newData, patches] = produceWithPatches(data, draft => {
221+
const dateBefore = draft.date
222+
draft.date = new Date()
223+
draft.other.x++
224+
draft.date = dateBefore
225+
})
226+
227+
// Expect patches and the returned value not to be the same reference
228+
expect(patches).not.toEqual([])
229+
expect(newData).not.toBe(data)
230+
})
231+
232+
it("nested in array", () => {
233+
type Item = {date: Date}
234+
235+
let initialDate = new Date()
236+
let data: Item[] = [{date: initialDate}]
237+
238+
const [newData, patches] = produceWithPatches(data, draft => {
239+
const element = draft[0]
240+
const dateBefore = element.date
241+
element.date = new Date()
242+
element.date = dateBefore
243+
})
244+
245+
// Expect no patches and the returned value to be the same reference
246+
expect(patches).toEqual([])
247+
expect(newData).toBe(data)
248+
})
249+
250+
it("nested in map", () => {
251+
type Item = {date: Date}
252+
253+
let initialDate = new Date()
254+
let data: Map<number, Item> = new Map([[0, {date: initialDate}]])
255+
256+
const [newData, patches] = produceWithPatches(data, draft => {
257+
const element = draft.get(0)!
258+
const dateBefore = element.date
259+
element.date = new Date()
260+
element.date = dateBefore
261+
})
262+
263+
// Expect no patches and the returned value to be the same reference
264+
expect(patches).toEqual([])
265+
expect(newData).toBe(data)
266+
})
267+
})
268+
180269
describe("curried producer", () => {
181270
it("supports rest parameters", () => {
182271
type State = {readonly a: 1}

src/core/proxy.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ import {
2222
ENUMERABLE,
2323
VALUE,
2424
isArray,
25-
isArrayIndex
25+
isArrayIndex,
26+
getProxyDraft,
27+
each,
28+
get,
29+
getValue
2630
} from "../internal"
2731

2832
interface ProxyBaseState extends ImmerBaseState {
@@ -203,6 +207,8 @@ export const objectTraps: ProxyHandler<ProxyState> = {
203207
)
204208
return true
205209

210+
if (revertToBaseIfNeeded(state, prop, value)) return true
211+
206212
// @ts-ignore
207213
state.copy_![prop] = value
208214
state.assigned_!.set(prop, true)
@@ -314,6 +320,42 @@ function getDescriptorFromProto(
314320
return undefined
315321
}
316322

323+
function revertToBaseIfNeeded(
324+
state: ImmerState,
325+
prop: PropertyKey,
326+
value: any
327+
): boolean {
328+
if (
329+
!has(state.base_, prop, state.type_) ||
330+
!is(get(state.base_, prop), getValue(value))
331+
) {
332+
return false
333+
}
334+
335+
state.copy_![prop] = value
336+
state.assigned_!.delete(prop)
337+
338+
if (state.assigned_!.size > 0) return true
339+
340+
let childModified = false
341+
each(state.copy_!, (key, val) => {
342+
if (getProxyDraft(val)?.modified_) {
343+
childModified = true
344+
}
345+
})
346+
if (childModified) return true
347+
348+
state.modified_ = false
349+
state.copy_ = null
350+
state.assigned_ = undefined
351+
352+
if (state.parent_ && state.key_ !== undefined) {
353+
revertToBaseIfNeeded(state.parent_, state.key_, state.base_)
354+
}
355+
356+
return true
357+
}
358+
317359
export function markChanged(state: ImmerState) {
318360
if (!state.modified_) {
319361
state.modified_ = true

0 commit comments

Comments
 (0)