Skip to content

Commit c0e6749

Browse files
committed
fix: reconcile if the original value is assigned after creating a draft. Fixes #659
1 parent 1b70ad5 commit c0e6749

2 files changed

Lines changed: 97 additions & 6 deletions

File tree

__tests__/regressions.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,5 +158,86 @@ function runBaseTest(name, useProxies, autoFreeze, useListener) {
158158
})
159159
expect(newData.foo[0].isActive).toBe(true)
160160
})
161+
162+
test("#659 no reconciliation after read", () => {
163+
const bar = {}
164+
const foo = {bar}
165+
166+
const next = produce(foo, draft => {
167+
draft.bar
168+
draft.bar = bar
169+
})
170+
expect(next).toBe(foo)
171+
})
172+
173+
test("#659 no reconciliation after read - 2", () => {
174+
const bar = {}
175+
const foo = {bar}
176+
177+
const next = produce(foo, draft => {
178+
const subDraft = draft.bar
179+
draft.bar = bar
180+
subDraft.x = 3 // this subDraft is not part of the end result, so ignore
181+
})
182+
183+
expect(next).toEqual(foo)
184+
})
185+
186+
test("#659 no reconciliation after read - 3", () => {
187+
const bar = {}
188+
const foo = {bar}
189+
190+
const next = produce(foo, draft => {
191+
const subDraft = draft.bar
192+
subDraft.x = 3 // this subDraft is not part of the end result, so ignore
193+
draft.bar = bar
194+
})
195+
expect(next).toEqual(foo)
196+
})
197+
198+
// Disabled: these are optimizations that would be nice if they
199+
// could be detected, but don't change the correctness of the result
200+
test.skip("#659 no reconciliation after read - 4", () => {
201+
const bar = {}
202+
const foo = {bar}
203+
204+
const next = produce(foo, draft => {
205+
const subDraft = draft.bar
206+
draft.bar = bar
207+
subDraft.x = 3 // this subDraft is not part of the end result, so ignore
208+
})
209+
210+
expect(next).toBe(foo)
211+
})
212+
213+
// Disabled: these are optimizations that would be nice if they
214+
// could be detected, but don't change the correctness of the result
215+
test.skip("#659 no reconciliation after read - 5", () => {
216+
const bar = {}
217+
const foo = {bar}
218+
219+
const next = produce(foo, draft => {
220+
const subDraft = draft.bar
221+
subDraft.x = 3 // this subDraft is not part of the end result, so ignore
222+
draft.bar = bar
223+
})
224+
expect(next).toBe(foo)
225+
})
226+
227+
test("#659 no reconciliation after read - 6", () => {
228+
const bar = {}
229+
const foo = {bar}
230+
231+
const next = produce(foo, draft => {
232+
const subDraft = draft.bar
233+
subDraft.x = 3 // this subDraft is not part of the end result, so ignore
234+
draft.bar = bar
235+
draft.bar = subDraft
236+
})
237+
expect(next).not.toBe(foo)
238+
expect(next).toEqual({
239+
bar: {x: 3}
240+
})
241+
})
161242
})
162243
}

src/core/proxy.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
ProxyTypeProxyObject,
1919
ProxyTypeProxyArray
2020
} from "../internal"
21+
import {isDraft} from "../utils/common"
2122

2223
interface ProxyBaseState extends ImmerBaseState {
2324
assigned_: {
@@ -129,28 +130,37 @@ export const objectTraps: ProxyHandler<ProxyState> = {
129130
ownKeys(state) {
130131
return Reflect.ownKeys(latest(state))
131132
},
132-
set(state, prop: string /* strictly not, but helps TS */, value) {
133+
set(
134+
state: ProxyObjectState,
135+
prop: string /* strictly not, but helps TS */,
136+
value
137+
) {
133138
const desc = getDescriptorFromProto(latest(state), prop)
134139
if (desc?.set) {
135140
// special case: if this write is captured by a setter, we have
136141
// to trigger it with the correct context
137142
desc.set.call(state.draft_, value)
138143
return true
139144
}
140-
state.assigned_[prop] = true
141145
if (!state.modified_) {
142146
// the last check is because we need to be able to distinguish setting a non-existig to undefined (which is a change)
143147
// from setting an existing property with value undefined to undefined (which is not a change)
144-
if (
145-
is(value, peek(latest(state), prop)) &&
146-
(value !== undefined || has(state.base_, prop))
147-
)
148+
const current = peek(latest(state), prop)
149+
// special case, if we assigning the original value to a draft, we can ignore the assignment
150+
const currentState: ProxyObjectState = current?.[DRAFT_STATE]
151+
if (currentState && currentState.base_ === value) {
152+
state.copy_![prop] = value
153+
state.assigned_[prop] = false
154+
return true
155+
}
156+
if (is(value, current) && (value !== undefined || has(state.base_, prop)))
148157
return true
149158
prepareCopy(state)
150159
markChanged(state)
151160
}
152161
// @ts-ignore
153162
state.copy_![prop] = value
163+
state.assigned_[prop] = true
154164
return true
155165
},
156166
deleteProperty(state, prop: string) {

0 commit comments

Comments
 (0)