Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

3 лет назад
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679
  1. <img src="images/immer-logo.png" height="200px" align="right"/>
  2. # Immer
  3. [![npm](https://img.shields.io/npm/v/immer.svg)](https://www.npmjs.com/package/immer) [![size](http://img.badgesize.io/https://cdn.jsdelivr.net/npm/immer/dist/immer.umd.js?compression=gzip)](http://img.badgesize.io/https://cdn.jsdelivr.net/npm/immer/dist/immer.umd.js) [![install size](https://packagephobia.now.sh/badge?p=immer)](https://packagephobia.now.sh/result?p=immer) [![Build Status](https://travis-ci.org/mweststrate/immer.svg?branch=master)](https://travis-ci.org/mweststrate/immer) [![Coverage Status](https://coveralls.io/repos/github/mweststrate/immer/badge.svg?branch=master)](https://coveralls.io/github/mweststrate/immer?branch=master) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/michelweststrate)
  4. _Create the next immutable state tree by simply modifying the current tree_
  5. Did Immer make a difference to your project? Consider buying me a coffee!<br/><a href="https://www.buymeacoffee.com/mweststrate" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: auto !important;width: auto !important;" ></a>
  6. ---
  7. * NPM: `npm install immer`
  8. * Yarn: `yarn add immer`
  9. * CDN: Exposed global is `immer`
  10. * Unpkg: `<script src="https://unpkg.com/immer/dist/immer.umd.js"></script>`
  11. * JSDelivr: `<script src="https://cdn.jsdelivr.net/npm/immer/dist/immer.umd.js"></script>`
  12. ---
  13. * Egghead lesson covering all of immer (7m): [Simplify creating immutable data trees with Immer](https://egghead.io/lessons/redux-simplify-creating-immutable-data-trees-with-immer)
  14. * Introduction blogpost: [Immer: Immutability the easy way](https://medium.com/@mweststrate/introducing-immer-immutability-the-easy-way-9d73d8f71cb3)
  15. * [Talk](https://www.youtube.com/watch?v=-gJbS7YjcSo) + [slides](http://immer.surge.sh/) on Immer at React Finland 2018 by Michel Weststrate
  16. * Blog: by Workday Prism on why they picked Immer to manage immutable state [The Search for a Strongly-Typed, Immutable State](https://medium.com/workday-engineering/workday-prism-analytics-the-search-for-a-strongly-typed-immutable-state-a09f6768b2b5)
  17. * Blog: [The Rise of Immer in React](https://www.netlify.com/blog/2018/09/12/the-rise-of-immer-in-react/)
  18. * Blog: [Immutability in React and Redux: The Complete Guide](https://daveceddia.com/react-redux-immutability-guide/)
  19. Immer (German for: always) is a tiny package that allows you to work with immutable state in a more convenient way. It is based on the [_copy-on-write_](https://en.wikipedia.org/wiki/Copy-on-write) mechanism.
  20. The basic idea is that you will apply all your changes to a temporarily _draftState_, which is a proxy of the _currentState_. Once all your mutations are completed, Immer will produce the _nextState_ based on the mutations to the draft state. This means that you can interact with your data by simply modifying it, while keeping all the benefits of immutable data.
  21. ![immer-hd.png](images/hd/immer.png)
  22. Using Immer is like having a personal assistant; he takes a letter (the current state), and gives you a copy (draft) to jot changes onto. Once you are done, the assistant will take your draft and produce the real immutable, final letter for you (the next state).
  23. A mindful reader might notice that this is quite similar to `withMutations` of ImmutableJS. It is indeed, but generalized and applied to plain, native JavaScript data structures (arrays and objects) without further needing any library.
  24. ## API
  25. The Immer package exposes a default function that does all the work.
  26. `produce(currentState, producer: (draftState) => void): nextState`
  27. There is also a curried overload that is explained [below](#currying).
  28. ## Example
  29. ```javascript
  30. import produce from "immer"
  31. const baseState = [
  32. {
  33. todo: "Learn typescript",
  34. done: true
  35. },
  36. {
  37. todo: "Try immer",
  38. done: false
  39. }
  40. ]
  41. const nextState = produce(baseState, draftState => {
  42. draftState.push({todo: "Tweet about it"})
  43. draftState[1].done = true
  44. })
  45. ```
  46. The interesting thing about Immer is that the `baseState` will be untouched, but the `nextState` will reflect all changes made to `draftState`.
  47. ```javascript
  48. // the new item is only added to the next state,
  49. // base state is unmodified
  50. expect(baseState.length).toBe(2)
  51. expect(nextState.length).toBe(3)
  52. // same for the changed 'done' prop
  53. expect(baseState[1].done).toBe(false)
  54. expect(nextState[1].done).toBe(true)
  55. // unchanged data is structurally shared
  56. expect(nextState[0]).toBe(baseState[0])
  57. // changed data not (dûh)
  58. expect(nextState[1]).not.toBe(baseState[1])
  59. ```
  60. ## Benefits
  61. * Immutability with normal JavaScript objects and arrays. No new APIs to learn!
  62. * Strongly typed, no string based paths selectors etc.
  63. * Structural sharing out of the box
  64. * Object freezing out of the box
  65. * Deep updates are a breeze
  66. * Boilerplate reduction. Less noise, more concise code.
  67. * Small: bundled and minified: 2KB.
  68. Read further to see all these benefits explained.
  69. ## Reducer Example
  70. Here is a simple example of the difference that Immer could make in practice.
  71. ```javascript
  72. // Redux reducer
  73. // Shortened, based on: https://github.com/reactjs/redux/blob/master/examples/shopping-cart/src/reducers/products.js
  74. const byId = (state, action) => {
  75. switch (action.type) {
  76. case RECEIVE_PRODUCTS:
  77. return {
  78. ...state,
  79. ...action.products.reduce((obj, product) => {
  80. obj[product.id] = product
  81. return obj
  82. }, {})
  83. }
  84. default:
  85. return state
  86. }
  87. }
  88. ```
  89. After using Immer, that simply becomes:
  90. ```javascript
  91. import produce from "immer"
  92. const byId = (state, action) =>
  93. produce(state, draft => {
  94. switch (action.type) {
  95. case RECEIVE_PRODUCTS:
  96. action.products.forEach(product => {
  97. draft[product.id] = product
  98. })
  99. }
  100. })
  101. ```
  102. Notice that it is not needed to handle the default case, a producer that doesn't do anything will simply return the original state.
  103. Creating Redux reducer is just a sample application of the Immer package. Immer is not just designed to simplify Redux reducers. It can be used in any context where you have an immutable data tree that you want to clone and modify (with structural sharing).
  104. _Note: it might be tempting after using producers for a while, to just place `produce` in your root reducer and then pass the draft to each reducer and work directly over such draft. Don't do that. It kills the point of Redux where each reducer is testable as pure reducer. Immer is best used when applying it to small individual pieces of logic._
  105. ## React.setState example
  106. Deep updates in the state of React components can be greatly simplified as well by using immer. Take for example the following onClick handlers (Try in [codesandbox](https://codesandbox.io/s/m4yp57632j)):
  107. ```javascript
  108. /**
  109. * Classic React.setState with a deep merge
  110. */
  111. onBirthDayClick1 = () => {
  112. this.setState(prevState => ({
  113. user: {
  114. ...prevState.user,
  115. age: prevState.user.age + 1
  116. }
  117. }))
  118. }
  119. /**
  120. * ...But, since setState accepts functions,
  121. * we can just create a curried producer and further simplify!
  122. */
  123. onBirthDayClick2 = () => {
  124. this.setState(
  125. produce(draft => {
  126. draft.user.age += 1
  127. })
  128. )
  129. }
  130. ```
  131. ## Currying
  132. Passing a function as the first argument to `produce` is intended to be used for currying. This means that you get a pre-bound producer that only needs a state to produce the value from. The producer function gets passed in the draft, and any further arguments that were passed to the curried function.
  133. For example:
  134. ```javascript
  135. // mapper will be of signature (state, index) => state
  136. const mapper = produce((draft, index) => {
  137. draft.index = index
  138. })
  139. // example usage
  140. console.dir([{}, {}, {}].map(mapper))
  141. //[{index: 0}, {index: 1}, {index: 2}])
  142. ```
  143. This mechanism can also nicely be leveraged to further simplify our example reducer:
  144. ```javascript
  145. import produce from 'immer'
  146. const byId = produce((draft, action) => {
  147. switch (action.type) {
  148. case RECEIVE_PRODUCTS:
  149. action.products.forEach(product => {
  150. draft[product.id] = product
  151. })
  152. return
  153. })
  154. }
  155. })
  156. ```
  157. Note that `state` is now factored out (the created reducer will accept a state, and invoke the bound producer with it).
  158. If you want to initialize an uninitialized state using this construction, you can do so by passing the initial state as second argument to `produce`:
  159. ```javascript
  160. import produce from "immer"
  161. const byId = produce(
  162. (draft, action) => {
  163. switch (action.type) {
  164. case RECEIVE_PRODUCTS:
  165. action.products.forEach(product => {
  166. draft[product.id] = product
  167. })
  168. return
  169. }
  170. },
  171. {
  172. 1: {id: 1, name: "product-1"}
  173. }
  174. )
  175. ```
  176. ## Patches
  177. During the run of a producer, Immer can record all the patches that would replay the changes made by the reducer.
  178. This is a very powerful tool if you want to fork your state temporarily, and replay the changes to the original.
  179. Patches are useful in few scenarios:
  180. * To exchange incremental updates with other parties, for example over websockets
  181. * For debugging / traces, to see precisely how state is changed over time
  182. * As basis for undo/redo or as approach to replay changes on a slightly different state tree
  183. To help with replaying patches, `applyPatches` comes in handy. Here is an example how patches could be used
  184. to record the incremental updates and (inverse) apply them:
  185. ```javascript
  186. import produce, {applyPatches} from "immer"
  187. let state = {
  188. name: "Micheal",
  189. age: 32
  190. }
  191. // Let's assume the user is in a wizard, and we don't know whether
  192. // his changes should be end up in the base state ultimately or not...
  193. let fork = state
  194. // all the changes the user made in the wizard
  195. let changes = []
  196. // the inverse of all the changes made in the wizard
  197. let inverseChanges = []
  198. fork = produce(
  199. fork,
  200. draft => {
  201. draft.age = 33
  202. },
  203. // The third argument to produce is a callback to which the patches will be fed
  204. (patches, inversePatches) => {
  205. changes.push(...patches)
  206. inverseChanges.push(...inversePatches)
  207. }
  208. )
  209. // In the mean time, our original state is replaced, as, for example,
  210. // some changes were received from the server
  211. state = produce(state, draft => {
  212. draft.name = "Michel"
  213. })
  214. // When the wizard finishes (successfully) we can replay the changes that were in the fork onto the *new* state!
  215. state = applyPatches(state, changes)
  216. // state now contains the changes from both code paths!
  217. expect(state).toEqual({
  218. name: "Michel", // changed by the server
  219. age: 33 // changed by the wizard
  220. })
  221. // Finally, even after finishing the wizard, the user might change his mind and undo his changes...
  222. state = applyPatches(state, inverseChanges)
  223. expect(state).toEqual({
  224. name: "Michel", // Not reverted
  225. age: 32 // Reverted
  226. })
  227. ```
  228. The generated patches are similar (but not the same) to the [RFC-6902 JSON patch standard](http://tools.ietf.org/html/rfc6902), except that the `path` property is an array, rather than a string.
  229. This makes processing patches easier. If you want to normalize to the official specification, `patch.path = patch.path.join("/")` should do the trick. Anyway, this is what a bunch of patches and their inverse could look like:
  230. ```json
  231. [
  232. { "op": "replace", "path": ["profile"], "value": { "name": "Veria", "age": 5 }},
  233. { "op": "remove", "path": ["tags", 3] }
  234. ]
  235. ```
  236. ```json
  237. [
  238. { "op": "replace", "path": ["profile"], "value": { "name": "Noa", "age": 6 }},
  239. { "op": "add", "path": ["tags", 3], "value": "kiddo"},
  240. ]
  241. ```
  242. ## Auto freezing
  243. 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.
  244. ## Returning data from producers
  245. It is not needed to return anything from a producer, as Immer will return the (finalized) version of the `draft` anyway. However, it is allowed to just `return draft`.
  246. It is also allowed to return arbitrarily other data from the producer function. But _only_ if you didn't modify the draft. This can be useful to produce an entirely new state. Some examples:
  247. ```javascript
  248. const userReducer = produce((draft, action) => {
  249. switch (action.type) {
  250. case "renameUser":
  251. // OK: we modify the current state
  252. draft.users[action.payload.id].name = action.payload.name
  253. return draft // same as just 'return'
  254. case "loadUsers":
  255. // OK: we return an entirely new state
  256. return action.payload
  257. case "adduser-1":
  258. // NOT OK: This doesn't do change the draft nor return a new state!
  259. // It doesn't modify the draft (it just redeclares it)
  260. // In fact, this just doesn't do anything at all
  261. draft = {users: [...draft.users, action.payload]}
  262. return
  263. case "adduser-2":
  264. // NOT OK: modifying draft *and* returning a new state
  265. draft.userCount += 1
  266. return {users: [...draft.users, action.payload]}
  267. case "adduser-3":
  268. // OK: returning a new state. But, unnecessary complex and expensive
  269. return {
  270. userCount: draft.userCount + 1,
  271. users: [...draft.users, action.payload]
  272. }
  273. case "adduser-4":
  274. // OK: the immer way
  275. draft.userCount += 1
  276. draft.users.push(action.payload)
  277. return
  278. }
  279. })
  280. ```
  281. _Note: It is not possible to return `undefined` this way, as it is indistinguishable from *not* updating the draft! Read on..._
  282. ## Producing `undefined` using `nothing`
  283. So, in general one can replace the current state by just `return`ing a new value from the producer, rather than modifying the draft.
  284. There is a subtle edge case however: if you try to write a producer that wants to replace the current state with `undefined`:
  285. ```javascript
  286. produce({}, draft => {
  287. // don't do anything
  288. })
  289. ```
  290. Versus:
  291. ```javascript
  292. produce({}, draft => {
  293. // Try to return undefined from the producer
  294. return undefined
  295. })
  296. ```
  297. The problem is that in JavaScript a function that doesn't return anything, also returns `undefined`!
  298. So immer cannot differentiate between those different cases.
  299. So, by default, Immer will assume that any producer that returns `undefined` just tried to modify the draft.
  300. However, to make it clear to Immer that you intentionally want to produce the value `undefined`, you can return the built-in token `nothing`:
  301. ```javascript
  302. import produce, { nothing } from "immer"
  303. const state = {
  304. hello: "world"
  305. }
  306. produce(state, (draft) => {})
  307. produce(state, (draft) => undefined)
  308. // Both return the original state: { hello: "world"}
  309. produce(state, (draft) => nothing)
  310. // Produces a new state, 'undefined'
  311. ```
  312. N.B. Note that this problem is specific for the `undefined` value, any other value, including `null`, doesn't suffer from this issue.
  313. ## Extracting the original object from a proxied instance
  314. Immer exposes a named export `original` that will get the original object from the proxied instance inside `produce` (or return `undefined` for unproxied values). A good example of when this can be useful is when searching for nodes in a tree-like state using strict equality.
  315. ```js
  316. const baseState = { users: [{ name: "Richie" }] };
  317. const nextState = produce(baseState, draftState => {
  318. original(draftState.users) // is === baseState.users
  319. })
  320. ```
  321. ## Using `this`
  322. The recipe will be always invoked with the `draft` as `this` context.
  323. This means that the following constructions are also valid:
  324. ```javascript
  325. const base = {counter: 0}
  326. const next = produce(base, function() {
  327. this.counter++
  328. })
  329. console.log(next.counter) // 1
  330. // OR
  331. const increment = produce(function() {
  332. this.counter++
  333. })
  334. console.log(increment(base).counter) // 1
  335. ```
  336. ## Inline shortcuts using `void`
  337. Draft mutations in Immer usually warrant a code block, since a return denotes an overwrite. Sometimes that can stretch code a little more than you might be comfortable with.
  338. In such cases you can use javascripts [`void`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/void) operator, which evaluates expressions and returns `undefined`.
  339. ```javascript
  340. // Single mutation
  341. produce(draft => void (draft.user.age += 1))
  342. // Multiple mutations
  343. produce(draft => void (draft.user.age += 1, draft.user.height = 186))
  344. ```
  345. Code style is highly personal, but for code bases that are to be understood by many, we recommend to stick to the classic `draft => { draft.user.age += 1}` to avoid cognitive overhead.
  346. ## TypeScript or Flow
  347. The Immer package ships with type definitions inside the package, which should be picked up by TypeScript and Flow out of the box and without further configuration.
  348. The TypeScript typings automatically remove `readonly` modifiers from your draft types and return a value that matches your original type. See this practical example:
  349. ```ts
  350. import produce from 'immer'
  351. interface State {
  352. readonly x: number
  353. }
  354. // `x` cannot be modified here
  355. const state: State = {
  356. x: 0
  357. }
  358. const newState = produce<State>(state, draft => {
  359. // `x` can be modified here
  360. draft.x++
  361. })
  362. // `newState.x` cannot be modified here
  363. ```
  364. This ensures that the only place you can modify your state is in your produce callbacks. It even works recursively and with `ReadonlyArray`s!
  365. ## Immer on older JavaScript environments?
  366. 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 on Android. In such cases Immer will fallback to an ES5 compatible implementation which works identical, but is a bit slower.
  367. ## Importing immer
  368. `produce` is exposed as the default export, but optionally it can be used as name import as well, as this benefits some older project setups. So the following imports are all correct, where the first is recommend:
  369. ```javascript
  370. import produce from "immer"
  371. import { produce } from "immer"
  372. const { produce } = require("immer")
  373. const produce = require("immer").produce
  374. const produce = require("immer").default
  375. import unleashTheMagic from "immer"
  376. import { produce as unleashTheMagic } from "immer"
  377. ```
  378. ## Limitations
  379. Immer supports the following types of data:
  380. 1. All kinds of primitive values
  381. 1. `Date` instances, but: only if not mutated, see below
  382. 1. Arrays, but: non-standard attributes are not supported (like: `array.test = "Test"`)
  383. 1. Plain objects (objects that have as prototype either `null` or `Object`)
  384. 1. Functions, assuming they aren't mutated
  385. 1. Other value types (like class instances) can be stored in the tree, but note that Immer won't work _inside_ those objects. In other words, if you modify a class instance, this will _not_ result in clone and unmodified original, but just in a modified original.
  386. # Pitfalls
  387. 1. Don't redefine draft like, `draft = myCoolNewState`. Instead, either modify the `draft` or return a new state. See [Returning data from producers](#returning-data-from-producers).
  388. 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.
  389. 1. Class instances are not, and will not supported first-class supported. Read [here](https://github.com/mweststrate/immer/issues/155#issuecomment-407725592) why classes are a conceptual mismatch (and technically extremely challenging)
  390. 1. For example, working with `Date` objects is no problem, just make sure you never modify them (by using methods like `setYear` on an existing instance). Instead, always create fresh `Date` instances. Which is probably what you were unconsciously doing already.
  391. 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.
  392. 1. Some debuggers (at least Node 6 is known) have trouble debugging when Proxies are in play. Node 8 is known to work correctly.
  393. 1. 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)})`
  394. 1. It is possible to return values from producers, except, it is not possible to return `undefined` that way, as it is indistiguishable from not updating the draft at all! If you want to replace the draft with `undefined`, just return `nothing` from the producer.
  395. 1. Immer does not support built in data-structures like `Map` and `Set`. However, it is fine to just immutably "update" them yourself but still leverage immer wherever possible:
  396. ```javascript
  397. const state = {
  398. title: "hello",
  399. tokenSet: new Set()
  400. }
  401. const nextState = produce(state, draft => {
  402. draft.title = draft.title.toUpperCase() // let immer do it's job
  403. // don't use the operations onSet, as that mutates the instance!
  404. // draft.tokenSet.add("c1342")
  405. // instead: clone the set (once!)
  406. const newSet = new Set(draft.tokenSet)
  407. // mutate the clone (just in this producer)
  408. newSet.add("c1342")
  409. // update the draft with the new set
  410. draft.tokenSet = newSet
  411. })
  412. ```
  413. Or a deep update in maps (well, don't use maps for this use case, but as example):
  414. ```javascript
  415. const state = {
  416. users: new Map(["michel", { name: "miche" }])
  417. }
  418. const nextState = produce(state, draft => {
  419. const newUsers = new Map(draft.users)
  420. // mutate the new map and set a _new_ user object
  421. // but leverage produce again to base the new user object on the original one
  422. newUsers.set("michel", produce(draft.users.get("michel"), draft => {
  423. draft.name = "michel"
  424. }))
  425. draft.users = newUsers
  426. })
  427. ```
  428. ## Cool things built with immer
  429. * [react-copy-write](https://github.com/aweary/react-copy-write) _Immutable state with a mutable API_
  430. * [redux-starter-kit](https://github.com/markerikson/redux-starter-kit) _A simple set of tools to make using Redux easier_
  431. * [immer based handleActions](https://gist.github.com/kitze/fb65f527803a93fb2803ce79a792fff8) _Boilerplate free actions for Redux_
  432. * [redux-box](https://github.com/anish000kumar/redux-box) _Modular and easy-to-grasp redux based state management, with least boilerplate_
  433. * [quick-redux](https://github.com/jeffreyyoung/quick-redux) _tools to make redux development quicker and easier_
  434. * [bey](https://github.com/jamiebuilds/bey) _Simple immutable state for React using Immer_
  435. * [immer-wieder](https://github.com/drcmda/immer-wieder#readme) _State management lib that combines React 16 Context and immer for Redux semantics_
  436. * [robodux](https://github.com/neurosnap/robodux) _flexible way to reduce redux boilerplate
  437. * ... and [many more](https://www.npmjs.com/browse/depended/immer)
  438. ## How does Immer work?
  439. Read the (second part of the) [introduction blog](https://medium.com/@mweststrate/introducing-immer-immutability-the-easy-way-9d73d8f71cb3).
  440. ## Example patterns.
  441. _For those who have to go back to thinking in object updates :-)_
  442. ```javascript
  443. import produce from "immer"
  444. // object mutations
  445. const todosObj = {
  446. id1: {done: false, body: "Take out the trash"},
  447. id2: {done: false, body: "Check Email"}
  448. }
  449. // add
  450. const addedTodosObj = produce(todosObj, draft => {
  451. draft["id3"] = {done: false, body: "Buy bananas"}
  452. })
  453. // delete
  454. const deletedTodosObj = produce(todosObj, draft => {
  455. delete draft["id1"]
  456. })
  457. // update
  458. const updatedTodosObj = produce(todosObj, draft => {
  459. draft["id1"].done = true
  460. })
  461. // array mutations
  462. const todosArray = [
  463. {id: "id1", done: false, body: "Take out the trash"},
  464. {id: "id2", done: false, body: "Check Email"}
  465. ]
  466. // add
  467. const addedTodosArray = produce(todosArray, draft => {
  468. draft.push({id: "id3", done: false, body: "Buy bananas"})
  469. })
  470. // delete
  471. const deletedTodosArray = produce(todosArray, draft => {
  472. draft.splice(draft.findIndex(todo => todo.id === "id1"), 1)
  473. // or (slower):
  474. // return draft.filter(todo => todo.id !== "id1")
  475. })
  476. // update
  477. const updatedTodosArray = produce(todosArray, draft => {
  478. draft[draft.findIndex(todo => todo.id === "id1")].done = true
  479. })
  480. ```
  481. ## Performance
  482. Here is a [simple benchmark](__performance_tests__/todo.js) on the performance of Immer. This test takes 50,000 todo items, and updates 5,000 of them. _Freeze_ indicates that the state tree has been frozen after producing it. This is a _development_ best practice, as it prevents developers from accidentally modifying the state tree.
  483. These tests were executed on Node 9.3.0. Use `yarn test:perf` to reproduce them locally.
  484. ![performance.png](images/performance.png)
  485. Most important observation:
  486. * Immer with proxies is roughly speaking twice to three times slower as a hand written reducer (the above test case is worst case, see `yarn test:perf` for more tests). This is in practice negligible.
  487. * Immer is roughly as fast as ImmutableJS. However, the _immutableJS + toJS_ makes clear the cost that often needs to be paid later; converting the immutableJS objects back to plain objects, to be able to pass them to components, over the network etc... (And there is also the upfront cost of converting data received from e.g. the server to immutable JS)
  488. * Generating patches doesn't significantly slow immer down
  489. * The ES5 fallback implementation is roughly twice as slow as the proxy implementation, in some cases worse.
  490. ## FAQ
  491. _(for those who skimmed the above instead of actually reading)_
  492. **Q: Does Immer use structural sharing? So that my selectors can be memoized and such?**
  493. A: Yes
  494. **Q: Does Immer support deep updates?**
  495. A: Yes
  496. **Q: I can't rely on Proxies being present on my target environments. Can I use Immer?**
  497. A: Yes
  498. **Q: Can I typecheck my data structures when using Immer?**
  499. A: Yes
  500. **Q: Can I store `Date` objects, functions etc in my state tree when using Immer?**
  501. A: Yes
  502. **Q: Is it fast?**
  503. A: Yes
  504. **Q: Idea! Can Immer freeze the state for me?**
  505. A: Yes
  506. ## Credits
  507. Special thanks goes to @Mendix, which supports it's employees to experiment completely freely two full days a month, which formed the kick-start for this project.
  508. ## Donations
  509. A significant part of my OSS work is unpaid. So [donations](https://mobx.js.org/donate.html) are greatly appreciated :)