Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Always freeze by default #702

Merged
merged 8 commits into from Nov 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions .travis.yml
@@ -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
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
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
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
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
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
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
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
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
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
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;
}