Skip to content

Commit

Permalink
feature: Always freeze by default (#702)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: always freeze by default, even in production mode. Use `setAutoFreeze(process.env.NODE_ENV !== 'production')` for the old behavior. See #687 (comment) for the rationale. Fixes #649, #681, #687
  • Loading branch information
mweststrate authored Nov 17, 2020
1 parent 6c62eec commit a406c8f
Show file tree
Hide file tree
Showing 11 changed files with 44 additions and 28 deletions.
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
language: node_js
node_js:
# - "10.18.1"
- "node"
- "10.18.1"
# - "node"
env:
- NODE_ENV=TEST
cache:
Expand Down
21 changes: 20 additions & 1 deletion __tests__/frozen.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import produce, {
setUseProxies,
setAutoFreeze,
enableAllPlugins
enableAllPlugins,
freeze
} from "../src/immer"

enableAllPlugins()
Expand Down Expand Up @@ -246,3 +247,21 @@ function runTests(name, useProxies) {
})
})
}

test("freeze - shallow", () => {
const obj1 = {hello: {world: true}}
const res = freeze(obj1)

expect(res).toBe(obj1)
expect(Object.isFrozen(res)).toBe(true)
expect(Object.isFrozen(res.hello)).toBe(false)
})

test("freeze - deep", () => {
const obj1 = {hello: {world: true}}
const res = freeze(obj1, true)

expect(res).toBe(obj1)
expect(Object.isFrozen(res)).toBe(true)
expect(Object.isFrozen(res.hello)).toBe(true)
})
3 changes: 2 additions & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ title: API overview
| `enableMapSet()` | Enables support for `Map` and `Set` collections. | [Installation](installation#pick-your-immer-version) |
| `enablePatches()` | Enables support for JSON patches. | [Installation](installation#pick-your-immer-version) |
| `finishDraft` | Given an draft created using `createDraft`, seals the draft and produces and returns the next immutable state that captures all the changes | [Async](async.md) |
| `freeze(obj, deep?)` | Freezes draftable objects. Returns the original object. By default freezes shallowly, but if the second argument is `true` it will freeze recursively. |
| `Immer` | constructor that can be used to create a second "immer" instance (exposing all APIs listed in this instance), that doesn't share its settings with global instance. |
| `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 | |
Expand All @@ -31,7 +32,7 @@ title: API overview
| `Patch` | Exposed TypeScript type, describes the shape of an (inverse) patch object | [Patches](patches.md) |
| `produce` | The core API of Immer, also exposed as the `default` export | [Produce](produce.md) |
| `produceWithPatches` | Works the same as `produce`, but instead of just returning the produced object, it returns a tuple, consisting of `[result, patches, inversePatches]`. | [Patches](patches.md) |
| `setAutoFreeze` | Enables / disables automatic freezing of the trees produces. By default enabled in development builds | [Freezing](freezing.md) |
| `setAutoFreeze` | Enables / disables automatic freezing of the trees produces. By default enabled. | [Freezing](freezing.md) |
| `setUseProxies` | Can be used to disable or force the use of `Proxy` objects. Useful when filing bug reports. | |

## Importing immer
Expand Down
4 changes: 3 additions & 1 deletion docs/freezing.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ title: Auto freezing
<a style="font-style:italic;padding:5px;margin:5px;" href="https://egghead.io/lessons/javascript-produces-immutable-data-and-avoid-unnecessary-creation-of-new-data-trees-with-immer">Hosted on egghead.io</a>
</details>

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. 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.
Immer automatically freezes any state trees that are modified using `produce`. This protects against accidental modifications of the state tree outside of a producer. In most cases this provides the most optimal behavior, but `setAutoFreeze(true / false)` can be used to explicitly turn this feature on or off.

Immer will never freeze (the contents of) non-enumerable, non-own or symbolic properties, unless their content was drafted.

_⚠️ Immer freezes everything recursively, for large data objects that won't be changed in the future this might be over-kill, in that case it can be more efficient to shallowly pre-freeze data using the `freeze` utility.⚠️_

_⚠️ 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! ⚠️_
2 changes: 1 addition & 1 deletion docs/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Most important observation:

### Pre-freeze data

When adding a large data set to the state tree in an Immer producer (for example data received from a JSON endpoint), it is worth to call `Object.freeze(json)` on the root of the data to be added first. This will allow Immer to add the new data to the tree faster, as it will skip freezing it, or searching the tree for any changes (drafts) that might be made.
When adding a large data set to the state tree in an Immer producer (for example data received from a JSON endpoint), it is worth to call `freeze(json)` on the root of the data to be added first. This will allow Immer to add the new data to the tree faster, as it will skip _recursively_ freezing it, or searching the new data for any changes (drafts) that might be made.

### You can always opt-out

Expand Down
3 changes: 1 addition & 2 deletions src/core/immerClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
getPlugin,
die,
hasProxies,
isMinified,
enterScope,
revokeScope,
leaveScope,
Expand All @@ -36,7 +35,7 @@ interface ProducersFns {
export class Immer implements ProducersFns {
useProxies_: boolean = hasProxies

autoFreeze_: boolean = __DEV__ ? true /* istanbul ignore next */ : !isMinified
autoFreeze_: boolean = true

constructor(config?: {useProxies?: boolean; autoFreeze?: boolean}) {
if (typeof config?.useProxies === "boolean")
Expand Down
3 changes: 2 additions & 1 deletion src/immer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export {
isDraft,
isDraftable,
NOTHING as nothing,
DRAFTABLE as immerable
DRAFTABLE as immerable,
freeze
} from "./internal"

const immer = new Immer()
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 @@ -109,3 +109,5 @@ declare export function enableES5(): void
declare export function enableMapSet(): void
declare export function enablePatches(): void
declare export function enableAllPlugins(): void

declare export function freeze<T>(obj: T, freeze?: boolean): T
13 changes: 11 additions & 2 deletions src/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,22 @@ export function shallowCopy(base: any) {
return Object.create(Object.getPrototypeOf(base), descriptors)
}

export function freeze(obj: any, deep: boolean): void {
if (isFrozen(obj) || isDraft(obj) || !isDraftable(obj)) return
/**
* Freezes draftable objects. Returns the original object.
* By default freezes shallowly, but if the second argument is `true` it will freeze recursively.
*
* @param obj
* @param deep
*/
export function freeze<T>(obj: T, deep?: boolean): T
export function freeze<T>(obj: any, deep: boolean = false): T {
if (isFrozen(obj) || isDraft(obj) || !isDraftable(obj)) return obj
if (getArchtype(obj) > 1 /* Map or Set */) {
obj.set = obj.add = obj.clear = obj.delete = dontMutateFrozenCollections as any
}
Object.freeze(obj)
if (deep) each(obj, (key, value) => freeze(value, true), true)
return obj
}

function dontMutateFrozenCollections() {
Expand Down
4 changes: 0 additions & 4 deletions src/utils/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ export const hasProxies =
typeof Proxy.revocable !== "undefined" &&
typeof Reflect !== "undefined"

/* istanbul ignore next */
function mini() {}
export const isMinified = mini.name !== "mini"

/**
* The sentinel value returned by producers to replace the draft with undefined.
*/
Expand Down
13 changes: 0 additions & 13 deletions website/static/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,3 @@ a {
a:hover {
text-decoration: underline;
}

/* BLM */
.slidingNav::before {
content: "Black Lives Matter";
position: fixed;
left: 0;
width: 100vw;
top: 13px;
text-align: center;
pointer-events: none;
text-transform: uppercase;
font-weight: bold;
}

0 comments on commit a406c8f

Please sign in to comment.