Skip to content

Commit

Permalink
docs: Organize performance and pitfalls, and document nested produce …
Browse files Browse the repository at this point in the history
…behavior. Fixes #694
  • Loading branch information
mweststrate committed Nov 17, 2020
1 parent 754331b commit 0730231
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 13 deletions.
18 changes: 15 additions & 3 deletions docs/performance.md
Expand Up @@ -42,6 +42,18 @@ Most important observation:

## Performance tips

- 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.
- Immer will convert anything you read in a draft recursively into a draft as well. If you have expensive side effect free operations on a draft that involves a lot of reading, for example finding an index using `find(Index)` in a very large array, you can speed this up by first doing the search, and only call the `produce` function once you know the index. Thereby preventing Immer to turn everything that was searched for in a draft. Or, perform the search on the original value of a draft, by using `original(someDraft)`, which boils to the same thing.
- Always try to pull produce 'up', for example `for (let x of y) produce(base, d => d.push(x))` is exponentially slower than `produce(base, d => { for (let x of y) d.push(x)})`
### 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.

### You can always opt-out

Realize that immer is opt-in everywhere, so it is perfectly fine to manually write super performance critical reducers, and use immer for all the normal ones. Even from within a producer you opt-out from Immer for certain parts of your logic by using utilies `original` or `current` and perform some of your operations on plain JavaScript objects.

### For expensive search operations, read from the original state, not the draft

Immer will convert anything you read in a draft recursively into a draft as well. If you have expensive side effect free operations on a draft that involves a lot of reading, for example finding an index using `find(Index)` in a very large array, you can speed this up by first doing the search, and only call the `produce` function once you know the index. Thereby preventing Immer to turn everything that was searched for in a draft. Or, alternatively, perform the search on the original value of a draft, by using `original(someDraft)`, which boils to the same thing.

### Pull produce as far up as possible

Always try to pull produce 'up', for example `for (let x of y) produce(base, d => d.push(x))` is exponentially slower than `produce(base, d => { for (let x of y) d.push(x)})`
57 changes: 47 additions & 10 deletions docs/pitfalls.md
Expand Up @@ -7,16 +7,37 @@ title: Pitfalls
<div data-ea-publisher="immerjs" data-ea-type="image" class="horizontal bordered"></div>
</center>

1. For performance tips, see [Performance Tips](https://immerjs.github.io/immer/docs/performance/#performance-tips).
1. Don't redefine draft like, `draft = myCoolNewState`. Instead, either modify the `draft` or return a new state. See [Returning data from producers](https://immerjs.github.io/immer/docs/return).
1. Immer assumes your state to be a unidirectional tree. That is, no object should appear twice in the tree, and there should be no circular references.
1. Since Immer uses proxies, reading huge amounts of data from state comes with an overhead (especially in the ES5 implementation). If this ever becomes an issue (measure before you optimize!), do the current state analysis before entering the producer function or read from the `currentState` rather than the `draftState`. Also, realize that immer is opt-in everywhere, so it is perfectly fine to manually write super performance critical reducers, and use immer for all the normal ones. Also note that `original` can be used to get the original state of an object, which is cheaper to read.
1. It is possible to return values from producers, except, it is not possible to return `undefined` that way, as it is indistinguishable from not updating the draft at all! If you want to replace the draft with `undefined`, just return `nothing` from the producer.
1. Immer [does not support exotic objects](https://github.com/immerjs/immer/issues/504) such as window.location.
1. You will need to enable your own classes to work properly with Immer. For docs on the topic, check out the section on [working with complex objects](https://immerjs.github.io/immer/docs/complex-objects).
1. For arrays, only numeric properties and the `length` property can be mutated. Custom properties are not preserved on arrays.
1. Note that data that comes from the closure, and not from the base state, will never be drafted, even when the data has become part of the new draft:
1. The set of patches generated by Immer should be correct, that is, applying them to an equal base object should result in the same end state. However Immer does not guarantee the generated set of patches will be optimal, that is, the minimum set of patches possible.
### Performance tipes

For performance tips, see [Performance Tips](https://immerjs.github.io/immer/docs/performance/#performance-tips).

### Don't reassign the recipe argument

Never reassign the `draft` argument (example: `draft = myCoolNewState`). Instead, either modify the `draft` or return a new state. See [Returning data from producers](https://immerjs.github.io/immer/docs/return).

### Immer only supports unidirectional trees

Immer assumes your state to be a unidirectional tree. That is, no object should appear twice in the tree, there should be no circular references. There should be exactly one path from the root to any node of the tree.

### Never explicitly return `undefined` from a producer

It is possible to return values from producers, except, it is not possible to return `undefined` that way, as it is indistinguishable from not updating the draft at all! If you want to replace the draft with `undefined`, just return `nothing` from the producer.

### Don't mutate exotic objects

Immer [does not support exotic objects](https://github.com/immerjs/immer/issues/504) such as window.location.

### Classes should be made draftable or not mutated

You will need to enable your own classes to work properly with Immer. For docs on the topic, check out the section on [working with complex objects](https://immerjs.github.io/immer/docs/complex-objects).

### Only valid indices and length can be mutated on Arrays

For arrays, only numeric properties and the `length` property can be mutated. Custom properties are not preserved on arrays.

### Data not originating from the state will never be drafted

Note that data that comes from the closure, and not from the base state, will never be drafted, even when the data has become part of the new draft.

```javascript
function onReceiveTodo(todo) {
Expand All @@ -33,3 +54,19 @@ function onReceiveTodo(todo) {
})
}
```

### Immer patches are not necessarily optimal

The set of patches generated by Immer should be correct, that is, applying them to an equal base object should result in the same end state. However Immer does not guarantee the generated set of patches will be optimal, that is, the minimum set of patches possible.

### Always use the result of nested producers

Nested `produce` calls are supported, but note that `produce` will _always_ produce a new state, so even when passing a draft to a nested produce, the changes made by the inner produce won't be visibile in the draft that was passed it, but only in the output that is produced. In other words, when using nested produce, you get a draft of a draft and the result of the inner produce should be merged back into the original draft (or returned). For example `produce(state, draft => { produce(draft.user, userDraft => { userDraft.name += "!" })})` won't work as the output if the inner produce isn't used. The correct way to use nested producers is:

This comment has been minimized.

Copy link
@RoccoC

RoccoC Nov 17, 2020

typo in visibile :)


```javascript
produce(state, draft => {
draft.user = produce(draft.user, userDraft => {
userDraft.name += "!"
})
})
```

1 comment on commit 0730231

@RoccoC
Copy link

@RoccoC RoccoC commented on 0730231 Nov 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The section on Always use the result of nested producers is helpful, thanks for adding!

Please sign in to comment.