Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions __tests__/isModified.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import {
produce,
createDraft,
finishDraft,
isModified,
enableMapSet
} from "../src/immer"

enableMapSet()

describe("isModified", () => {
it("returns false for values that are not drafts", () => {
expect(isModified({})).toBe(false)
expect(isModified([])).toBe(false)
expect(isModified(null)).toBe(false)
expect(isModified(undefined)).toBe(false)
expect(isModified(42)).toBe(false)
expect(isModified("draft")).toBe(false)
const next = produce({x: 1}, draft => {
draft.x = 2
})
expect(isModified(next)).toBe(false)
})

it("returns false for an unmodified draft", () => {
produce({item: {count: 1}}, draft => {
expect(isModified(draft)).toBe(false)
expect(isModified(draft.item)).toBe(false)
})
})

it("returns true after a direct modification", () => {
produce({x: 1}, draft => {
draft.x = 2
expect(isModified(draft)).toBe(true)
})
})

it("returns true after adding or deleting a property", () => {
produce({x: 1}, draft => {
draft.y = 2
expect(isModified(draft)).toBe(true)
})
produce({x: 1}, draft => {
delete draft.x
expect(isModified(draft)).toBe(true)
})
})

it("returns false when the same value is assigned back", () => {
produce({x: 1}, draft => {
draft.x = 1
expect(isModified(draft)).toBe(false)
})
})

it("tracks nested drafts and their parents, but not untouched siblings", () => {
const base = {
item: {count: 1},
other: {count: 10}
}
produce(base, draft => {
draft.item.count = 2
expect(isModified(draft.item)).toBe(true)
// modifications propagate to parent drafts
expect(isModified(draft)).toBe(true)
expect(isModified(draft.other)).toBe(false)
})
})

it("tracks array modifications", () => {
produce([1, 2, 3], draft => {
expect(isModified(draft)).toBe(false)
draft.push(4)
expect(isModified(draft)).toBe(true)
})
produce({list: [1, 2, 3]}, draft => {
draft.list[0] = 99
expect(isModified(draft.list)).toBe(true)
expect(isModified(draft)).toBe(true)
})
})

it("works with manually created drafts", () => {
const draft = createDraft({item: {count: 1}})
expect(isModified(draft)).toBe(false)
expect(isModified(draft.item)).toBe(false)
draft.item.count = 2
expect(isModified(draft.item)).toBe(true)
expect(isModified(draft)).toBe(true)
const next = finishDraft(draft)
expect(next.item.count).toBe(2)
})

it("tracks Map and Set drafts", () => {
const base = {
map: new Map([["a", 1]]),
set: new Set([1])
}
produce(base, draft => {
expect(isModified(draft.map)).toBe(false)
draft.map.set("b", 2)
expect(isModified(draft.map)).toBe(true)
expect(isModified(draft)).toBe(true)
expect(isModified(draft.set)).toBe(false)
draft.set.add(2)
expect(isModified(draft.set)).toBe(true)
})
})

it("supports the derived-data use case from the proposal", () => {
const base = {item: {count: 1, modifiedOn: null}}
const next = produce(base, draft => {
draft.item.count++
if (isModified(draft.item)) {
draft.item.modifiedOn = "now"
}
})
expect(next.item.modifiedOn).toBe("now")

const untouched = produce(base, draft => {
if (isModified(draft.item)) {
draft.item.modifiedOn = "now"
}
})
expect(untouched).toBe(base)
})
})
1 change: 1 addition & 0 deletions src/immer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export {
current,
isDraft,
isDraftable,
isModified,
NOTHING as nothing,
DRAFTABLE as immerable,
freeze,
Expand Down
2 changes: 2 additions & 0 deletions src/types/index.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ declare export function current<S>(value: S): S

declare export function isDraft(value: any): boolean

declare export function isModified(value: any): boolean

/**
* Creates a mutable draft from an (immutable) object / array.
* The draft can be modified until `finishDraft` is called
Expand Down
13 changes: 13 additions & 0 deletions src/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ export const VALUE = "value"
/*#__PURE__*/
export let isDraft = (value: any): boolean => !!value && !!value[DRAFT_STATE]

/**
* Returns true if the given value is an Immer draft that has been modified,
* either directly or through one of its nested drafts. Modifications made to
* a nested draft mark all of its parent drafts as modified as well, so this
* also reports modifications that happened anywhere inside the given draft.
* Returns false for values that are not drafts.
*/
/*#__PURE__*/
export function isModified(value: any): boolean {
if (!isDraft(value)) return false
return value[DRAFT_STATE].modified_
}

/** Returns true if the given value can be drafted by Immer */
/*#__PURE__*/
export function isDraftable(value: any): boolean {
Expand Down
1 change: 1 addition & 0 deletions website/docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ title: API overview
| `immerable` | Symbol that can be added to a constructor or prototype, to indicate that Immer should treat the class as something that can be safely drafted | [Classes](./complex-objects.md) |
| `Immutable<T>` | Exposed TypeScript type to convert mutable types to immutable types | |
| `isDraft` | Returns true if the given object is a draft object | |
| `isModified` | Returns true if the given object is a draft object that has been modified, either directly or through one of its nested drafts. Returns false for values that are not drafts | |
| `isDraftable` | Returns true if Immer is capable of turning this object into a draft. Which is true for: arrays, objects without prototype, objects with `Object` as their prototype, objects that have the `immerable` symbol on their constructor or prototype | |
| `nothing` | Value that can be returned from a recipe, to indicate that the value `undefined` should be produced | [Return](./return.mdx) |
| `original` | Given a draft object (doesn't have to be a tree root), returns the original object at the same path in the original state tree, if present | [Original](./original.md) |
Expand Down