Mutative

Migration from Mutative v1 to v2

The changes since Mutative 1.3.0 that can affect existing code, in builds, patches, arrays, returned values, development builds, types and fixes.

Mutative v2 keeps the v1 API. The changes below, made since v1.3.0, can affect existing code.

Build output and package entries

  • ES2018 output. Every build targets ES2018 instead of ES2015, so object spread stays native. Engines that support Proxy but not ES2018 syntax, such as Chrome 49–59, Firefox 18–54, Safari 10–11.0 and Edge 12–18, need Mutative transpiled by your build.
  • Bundlers. The ESM entry compares process.env.NODE_ENV at each development check instead of always running development code. Production bundles, in which the bundler defines NODE_ENV as production, drop the development checks and warnings and throw minified errors, as CommonJS consumers already did. Bundles that target Node.js without defining NODE_ENV, such as esbuild with platform: 'node', now include both CommonJS builds; define NODE_ENV or keep Mutative external.
  • Node.js ESM. In Node.js, import resolves a module that re-exports the CommonJS build, so import and require share one instance and both follow NODE_ENV. Previously, import always ran the development ESM build as a separate instance.
  • Browsers without a bundler. dist/mutative.esm.mjs and dist/mutative.esm.js read process.env.NODE_ENV, so a page that loads them directly, for example from a raw CDN URL, fails with process is not defined. Load dist/mutative.esm.production.min.mjs instead, or the UMD build that the unpkg and jsdelivr fields point to.
  • Production errors. Production builds throw Minified Mutative error #<code> with a link to the errors page instead of the full message, also for apply(), original() and rawReturn(). Development builds keep the messages. Code that matches error messages in production must match the codes instead.

Patches

  • Order. The patches of a nested draft now come before those of its parents, the order Immer uses. For example, const item = draft.list[0]; draft.list.length = 0; item.text = 'b'; draft.item = item; yields the list length replace before the item add. Applying the patches gives the same state as before, and replace patches that only restated an unchanged draft at its original index are no longer emitted.
  • Values. A patch value that was a draft is now the object that the next state holds instead of a deep copy, and it is frozen with the state when enableAutoFreeze is on. apply() still copies patch values before applying them. Copy a patch value before mutating it.
  • Moved drafts. A changed draft that leaves its key, because sort() or an assignment moved it or another value replaced it there, no longer emits patches under its old path; the patches of its parent carry its value. In v1, the patches of such a recipe could fail to apply, for example the inverse patches once a primitive took the old key.
  • Set items. A changed item of a Set that also added or removed items is carried by the Set's remove and add patches. v1 also emitted patches under the item's position in the changed Set, which replay applied to whatever sat at that position in the base Set, or could not apply at all. A Set that only changed its items keeps the patches under each item's position.
  • Original objects assigned over drafts. Assigning the original object of a draft to the key that holds the draft is recorded like any other assignment: when the draft was read at that key, the key holds its original value again and emits no patch; when the draft was moved there from another key, the key emits the patch of that assignment. In v1, both cases emitted a remove patch for an object key that the state kept, and the second emitted no patch for an array element, so replaying the patches dropped the key or lost the element.

Arrays

  • shift, unshift, splice and reverse run natively on the draft's copy of a plain array, and indexOf, lastIndexOf and includes search it natively; see which array methods run natively. For data arrays, the results are those of v1 and the patches replay in both directions. An accessor property on an array index is read as a data value, so its getter may run a different number of times.
  • current() of a changed array copies the current array instead of reading every element through the draft. Base elements that a native method moved and that hold drafts of an outer create() call are left as they are, as unmoved elements always were.
  • To search a large array without drafting each element it visits, search current(draft.list) and change the match through the draft; see current().

Returned values

  • With enableAutoFreeze, production builds no longer search frozen objects in a value returned from a recipe, the returned value included, or the values they hold, for drafts, and leave a draft there unresolved. Development builds still search them and throw when they find one, also in an unfrozen object that a frozen one holds, where v1 replaced it. Never put a draft in a frozen object or in a value it holds. Without auto-freeze, returned values are searched as in v1.

Development builds

  • In strict mode, development builds warn once when a recipe leaves 1,000 or more drafts unchanged, as a search through a large draft array does.
  • In strict mode, rawReturn() of a value without drafts no longer prints contradictory warnings.
  • With enablePatches: { pathAsArray: false }, development builds throw when a patch path would hold a Map key that is not a string, or a symbol key, as a string path cannot name such a key: applying the patch writes another key, for example '1' instead of 1, or fails for a change below that key. Production builds still generate such patches, as v1 did, and still throw a TypeError for a symbol key. Keep the default array paths for such keys.
  • With enableAutoFreeze, development builds report a circular reference only for an object that holds itself. A changed draft that holds its original, as draft.prev = original(draft) makes it, or that links to a shared object through another path no longer throws; production builds always froze such states. For a real cycle, the error names the path of the object that repeats, which can be one key longer than in v1.
  • Development builds throw when a recipe passes a draft to the set() method of a Map draft, as in draft.map.set(draft.key, value): Map keys are used as they are, so the next state would keep the draft as a key, revoked, and reading its properties would throw. Production builds still keep such a key, as v1 did. Use original(draft), current(draft) or an id as the key.

TypeScript

  • create() with an explicit state type and an async recipe, such as create<State>(base, async (draft) => { … }), returns Promise<State>, and so do curried producers; v1 typed the result as State. A state type that is a type parameter, as in a generic function, or that includes Promises, such as object or unknown, still gives the state type for async recipes and for recipes that may return a Promise, as in v1; leave out the type argument in generic code.
  • apply() accepts enableAutoFreeze: true and then returns Immutable<State>, as apply<State, true>() does; v1 rejected the option and typed the frozen result as mutable. With enableAutoFreeze typed as boolean or optional true, the result is State | Immutable<State>. With mutable typed as boolean, optional boolean, or optional true, apply() returns State | void.
  • A curried producer whose recipe annotates its draft and returns a new state, such as create((draft: State) => ({ ...draft, count: 0 })), returns State, or Promise<State> for an async recipe. v1 typed such a call as a manual draft, so calling the producer did not compile. A recipe whose draft is annotated with a type parameter, as in create((draft: T) => { … }) in a generic function, also gives a producer, which v1 typed as a manual draft as well.
  • With an explicit state type, a recipe that may or may not return a Promise, such as one typed (draft: State) => void | Promise<void>, gives State | Promise<State>, also for a primitive state type, such as number, and in curried producers and creators from makeCreator(). v1 typed the result as State.

Fixes that change results

  • With enablePatches, every changed item of a Set keeps its changes. In v1, when two or more items of a Set that was not the root changed below their first level, the state kept the change of only one of them.
  • A Set that receives an unchanged draft holds the original object, as the rest of the state does, instead of the draft's shallow copy.
  • A Set draft that the recipe moves to another place, such as another key, an array, a Map, another Set or a new object, keeps its added, deleted and changed items there when its old place gets another value or is removed; v1 kept its original items.
  • A draft of a Set subclass changes its copy, an instance of the subclass, as Map drafts do: the subclass's own add, has, delete and clear take effect during the recipe, while the draft iterates the items in the order in which the Set holds them. v1 kept the items of a changed Set in a separate map, which bypassed those methods until it rebuilt the Set after the recipe. As a Map subclass's set already had to in v1, a Set subclass's add must store the value that it receives: an add that stores a copy of a draft, or of an object that holds drafts, leaves those drafts in the next state, where reading them throws.
  • Assigning undefined to a key that delete, shift, unshift or a shrinking splice removed from a draft adds the key back; v1 left a hole or kept the shorter length.
  • Under a mark that returns mutable, a value that the recipe assigned or moved is read back as assigned, through the draft and in current(); v1 returned the original value.
  • current() of a draft whose state holds a plain Set with drafts returns a snapshot; v1 threw.
  • current() snapshots an object that the recipe assigned and that mark makes draftable, such as a class instance, together with the drafts it holds; v1 returned the object as it was, so a later change to those drafts showed through the snapshot.
  • With enableAutoFreeze, Map and Set instances are frozen too, so adding a property or replacing a method fails as on any frozen object; v1 only replaced their mutators. Later producers skip a frozen Map or Set instead of walking its entries again, and a Map or Set that holds itself no longer overflows the stack in production builds.
  • The iterators that Map and Set drafts return behave like built-in iterators: iterating one that was partly consumed continues where it stopped, and iterator helpers such as toArray() are available where the engine has them. In v1, iterating such an iterator started over, and only a Map's keys() had the helpers.
  • Consume Map value and entry iterators, and all Set iterators, while their draft is active. After the producer finishes or fails, or a manual draft is finalized, an iterator cannot yield another value and throws a TypeError. Lazy iterator helpers follow the same lifetime; an exhausted iterator stays exhausted. Map keys are not drafted, and keys() continues to return a native iterator.
  • apply() copies the own symbol keys of patch values, and an own __proto__ key, as JSON.parse() creates one, stays a data property. v1 dropped symbol keys there and turned an own __proto__ key into the prototype of the copy.
  • apply() rejects a patch whose path ends in __proto__ on an object or array, as it rejects __proto__ earlier in a path. Coercible property keys, such as a boxed string, are checked after conversion too. With mutable: true, v1 assigned the value and so replaced the prototype of the object.
  • In a patch path, apply() reads an item of a Set only at its index, such as 1 or '1', and throws for any other key, such as __proto__. v1 read any property of an array of the Set's items there, which let a path through __proto__ reach Array.prototype.
  • apply() with mutable: true throws for a patch that replaces the root state with another value, such as the patch of a recipe that returns a new state, as the state cannot be replaced in place. v1 ignored the replacement and applied the patches after it to the replacing value, so the state stayed as it was and the value of that patch changed.
  • An object that mark makes draftable, such as a plain object under markSimpleObject, keeps an own __proto__ key, as JSON.parse() creates one, as a property of its copy; v1 assigned it, which replaced the prototype of the copy.
  • A patch for a Map key that is an array holds the key as one path segment; v1 spread the array into the path, so applying the patch wrote to other keys.
  • apply() uses a Map key of a patch path as it is. v1 converted it to a string first, which threw for an object without a prototype.
  • Array patches record a change between 0 and -0, which the state already kept; in v1, applying the patches lost the sign. An element that stays NaN no longer yields a replace patch.
  • When a recipe shortens an array and lengthens it again, as draft.length = 1; draft.length = 3 does, the patches replace the indices that the longer length exposes again. In v1, applying the patches kept the removed elements at those indices, and applying the inverse patches left holes there.
  • In strict mode, a nested unsafe() call no longer ends the access of the outer call; in v1, reading mutable data after it in the outer callback threw.
  • A producer that fails after its recipe returned, for example because the recipe changed the draft and returned another value, revokes its drafts and releases its array method cache, as a recipe that throws does; v1 left them usable. This also covers errors while inspecting a returned Proxy or calling a returned Promise's then method, and preserves the original error.
  • A draft of an array whose Symbol.isConcatSpreadable is false copies its elements; v1 put the whole array into the copy as its only element.
Edit on GitHub

Last updated on by unadlib

On this page