Skip to content

Commit

Permalink
Implemented deepFreeze functionality, *anything draftable* returned f…
Browse files Browse the repository at this point in the history
…rom a produce will now be deeply frozen
  • Loading branch information
mweststrate committed Sep 11, 2019
1 parent 8aa4da3 commit 7fd3ef3
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 29 deletions.
50 changes: 24 additions & 26 deletions __tests__/frozen.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,6 @@ function runTests(name, useProxies) {
expect(isFrozen(next.a)).toBeTruthy()
})

it("a new object replaces a primitive base", () => {
const obj = {a: {}}
const next = produce(null, () => obj)
expect(next).toBe(obj)
expect(isFrozen(next)).toBeTruthy()
expect(isFrozen(next.a)).toBeTruthy()
})

it("a new object replaces the entire draft", () => {
const obj = {a: {b: {}}}
const next = produce({}, () => obj)
Expand Down Expand Up @@ -90,37 +82,43 @@ function runTests(name, useProxies) {
expect(isFrozen(next.a.b)).toBeTruthy()
expect(isFrozen(next.a.b.c)).toBeTruthy()
})
})

describe("the result is never auto-frozen when", () => {
it("a nested draft is returned", () => {
const base = {a: {}}
const next = produce(base, draft => draft.a)
expect(next).toBe(base.a)
expect(isFrozen(next)).toBeTruthy()
})

it("the base state is returned", () => {
const base = {}
const next = produce(base, () => base)
expect(next).toBe(base)
expect(isFrozen(next)).toBeTruthy()
})

it("the producer is a no-op", () => {
const base = {a: {}}
const next = produce(base, () => {})
expect(next).toBe(base)
expect(isFrozen(next)).toBeFalsy()
expect(isFrozen(next.a)).toBeFalsy()
expect(isFrozen(next)).toBeTruthy()
expect(isFrozen(next.a)).toBeTruthy()
})

it("the root draft is returned", () => {
const base = {a: {}}
const next = produce(base, draft => draft)
expect(next).toBe(base)
expect(isFrozen(next)).toBeFalsy()
expect(isFrozen(next.a)).toBeFalsy()
})

it("a nested draft is returned", () => {
const base = {a: {}}
const next = produce(base, draft => draft.a)
expect(next).toBe(base.a)
expect(isFrozen(next)).toBeFalsy()
expect(isFrozen(next)).toBeTruthy()
expect(isFrozen(next.a)).toBeTruthy()
})

it("the base state is returned", () => {
const base = {}
const next = produce(base, () => base)
expect(next).toBe(base)
expect(isFrozen(next)).toBeFalsy()
it("a new object replaces a primitive base", () => {
const obj = {a: {}}
const next = produce(null, () => obj)
expect(next).toBe(obj)
expect(isFrozen(next)).toBeTruthy()
expect(isFrozen(next.a)).toBeTruthy()
})
})

Expand Down
2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,8 @@ isDraft(nextState) // => false

Immer automatically freezes any state trees that are modified using `produce`. This protects against accidental modifications of the state tree outside of a producer. This comes with a performance impact, so it is recommended to disable this option in production. It is by default enabled. By default, it is turned on during local development and turned off in production. Use `setAutoFreeze(true / false)` to explicitly turn this feature on or off.

_⚠️ If auto freezing is enabled, recipes are not entirely side-effect free: Any plain object or array that ends up in the produced result, will be frozen, even when these objects were not frozen before the start of the producer! ⚠️_

## Immer on older JavaScript environments?

By default `produce` tries to use proxies for optimal performance. However, on older JavaScript engines `Proxy` is not available. For example, when running Microsoft Internet Explorer or React Native (< v0.59) on Android. In such cases, Immer will fallback to an ES5 compatible implementation which works identical, but is a bit slower.
Expand Down
9 changes: 9 additions & 0 deletions src/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,12 @@ export function clone(obj) {
for (const key in obj) cloned[key] = clone(obj[key])
return cloned
}

export function deepFreeze(obj) {
if (!isDraftable(obj)) return
if (Object.isFrozen(obj)) return
if (isDraft(obj)) return
Object.freeze(obj)
if (Array.isArray(obj)) obj.forEach(deepFreeze)
for (const key in obj) deepFreeze(obj[key])
}
19 changes: 16 additions & 3 deletions src/immer.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
isEnumerable,
shallowCopy,
DRAFT_STATE,
NOTHING
NOTHING,
deepFreeze
} from "./common"
import {ImmerScope} from "./scope"

Expand Down Expand Up @@ -87,8 +88,10 @@ export class Immer {
return this.processResult(result, scope)
} else {
result = recipe(base)
if (result === undefined) return base
return result !== NOTHING ? result : undefined
if (result === NOTHING) return undefined
if (result === undefined) result = base
this.maybeFreeze(result, true)
return result
}
}
produceWithPatches(arg1, arg2, arg3) {
Expand Down Expand Up @@ -170,6 +173,7 @@ export class Immer {
if (isDraftable(result)) {
// Finalize the result in case it contains (or is) a subset of the draft.
result = this.finalize(result, null, scope)
this.maybeFreeze(result)
}
if (scope.patches) {
scope.patches.push({
Expand Down Expand Up @@ -209,6 +213,7 @@ export class Immer {
return draft
}
if (!state.modified) {
this.maybeFreeze(state.base, true)
return state.base
}
if (!state.finalized) {
Expand Down Expand Up @@ -299,6 +304,8 @@ export class Immer {
// Search new objects for unfinalized drafts. Frozen objects should never contain drafts.
else if (isDraftable(value) && !Object.isFrozen(value)) {
each(value, finalizeProperty)
// for externally incoming object trees, we wan't to make sure they're frozen automatically
this.maybeFreeze(value)
}

if (isDraftProp && this.onAssign) {
Expand All @@ -309,4 +316,10 @@ export class Immer {
each(root, finalizeProperty)
return root
}
maybeFreeze(value, deep = false) {
if (this.autoFreeze && !isDraft(value)) {
if (deep) deepFreeze(value)
else Object.freeze(value)
}
}
}

0 comments on commit 7fd3ef3

Please sign in to comment.