diff --git a/src/immer.js b/src/immer.js index c0a8290b..53346569 100644 --- a/src/immer.js +++ b/src/immer.js @@ -20,7 +20,10 @@ const configDefaults = { autoFreeze: typeof process !== "undefined" ? process.env.NODE_ENV !== "production" - : verifyMinified.name === "verifyMinified" + : verifyMinified.name === "verifyMinified", + onAssign: null, + onDelete: null, + onCopy: null } export class Immer { @@ -131,6 +134,12 @@ export class Immer { if (!state.finalized) { state.finalized = true this.finalizeTree(state.draft, path, patches, inversePatches) + if (this.onDelete) { + const {assigned} = state + for (const prop in assigned) + assigned[prop] || this.onDelete(state, prop) + } + if (this.onCopy) this.onCopy(state) if (this.autoFreeze) Object.freeze(state.copy) if (patches) generatePatches(state, path, patches, inversePatches) } @@ -147,20 +156,35 @@ export class Immer { ? state.copy : (state.copy = shallowCopy(state.draft)) } + const {onAssign} = this const finalizeProperty = (prop, value, parent) => { - // Skip unchanged properties in draft objects. - if (state && parent === root && is(value, state.base[prop])) return - if (!isDraftable(value)) return - if (!isDraft(value)) { - // Frozen values are already finalized. - return Object.isFrozen(value) || each(value, finalizeProperty) + // Only `root` can be a draft in here. + const inDraft = state && parent === root + if (isDraftable(value)) { + // Skip unchanged properties in draft objects. + if (inDraft && is(value, state.base[prop])) return + + // Find proxies within new objects. Frozen objects are already finalized. + if (!isDraft(value)) { + if (!Object.isFrozen(value)) each(value, finalizeProperty) + if (onAssign && inDraft) onAssign(state, prop, value) + return + } + + // prettier-ignore + parent[prop] = + // Patches are never generated for assigned properties. + patches && parent === root && !(state && has(state.assigned, prop)) + ? this.finalize(value, path.concat(prop), patches, inversePatches) + : this.finalize(value) + + if (onAssign && inDraft) onAssign(state, prop, parent[prop]) + return + } + // Call `onAssign` for primitive values. + if (onAssign && inDraft && !is(value, state.base[prop])) { + onAssign(state, prop, value) } - // prettier-ignore - parent[prop] = - // Patches are never generated for assigned properties. - patches && parent === root && !(state && has(state.assigned, prop)) - ? this.finalize(value, path.concat(prop), patches, inversePatches) - : this.finalize(value) } each(root, finalizeProperty) return root diff --git a/src/index.d.ts b/src/index.d.ts index 899b3893..e2ab2da3 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -130,7 +130,13 @@ export function original(value: T): T | void export function isDraft(value: any): boolean export class Immer { - constructor(config: {useProxies?: boolean; autoFreeze?: boolean}) + constructor(config: { + useProxies?: boolean + autoFreeze?: boolean + onAssign?: (state: ImmerState, prop: keyof any, value: any) => void + onDelete?: (state: ImmerState, prop: keyof any) => void + onCopy?: (state: ImmerState) => void + }) /** * The `produce` function takes a value and a "recipe function" (whose * return value often depends on the base state). The recipe function is @@ -173,3 +179,10 @@ export class Immer { */ setUseProxies(useProxies: boolean): void } + +export interface ImmerState { + parent?: ImmerState + base: T + copy: T + assigned: {[prop: string]: boolean} +}