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_ENVat each development check instead of always running development code. Production bundles, in which the bundler definesNODE_ENVasproduction, drop the development checks and warnings and throw minified errors, as CommonJS consumers already did. Bundles that target Node.js without definingNODE_ENV, such as esbuild withplatform: 'node', now include both CommonJS builds; defineNODE_ENVor keep Mutative external. - Node.js ESM. In Node.js,
importresolves a module that re-exports the CommonJS build, soimportandrequireshare one instance and both followNODE_ENV. Previously,importalways ran the development ESM build as a separate instance. - Browsers without a bundler.
dist/mutative.esm.mjsanddist/mutative.esm.jsreadprocess.env.NODE_ENV, so a page that loads them directly, for example from a raw CDN URL, fails withprocess is not defined. Loaddist/mutative.esm.production.min.mjsinstead, or the UMD build that theunpkgandjsdelivrfields point to. - Production errors. Production builds throw
Minified Mutative error #<code>with a link to the errors page instead of the full message, also forapply(),original()andrawReturn(). 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 thelistlength replace before theitemadd. 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
enableAutoFreezeis 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
removeandaddpatches. 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
removepatch 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,spliceandreverserun natively on the draft's copy of a plain array, andindexOf,lastIndexOfandincludessearch 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 outercreate()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; seecurrent().
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 of1, or fails for a change below that key. Production builds still generate such patches, as v1 did, and still throw aTypeErrorfor 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, asdraft.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 indraft.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. Useoriginal(draft),current(draft)or an id as the key.
TypeScript
create()with an explicit state type and an async recipe, such ascreate<State>(base, async (draft) => { … }), returnsPromise<State>, and so do curried producers; v1 typed the result asState. A state type that is a type parameter, as in a generic function, or that includes Promises, such asobjectorunknown, 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()acceptsenableAutoFreeze: trueand then returnsImmutable<State>, asapply<State, true>()does; v1 rejected the option and typed the frozen result as mutable. WithenableAutoFreezetyped asbooleanor optionaltrue, the result isState | Immutable<State>. Withmutabletyped asboolean, optionalboolean, or optionaltrue,apply()returnsState | void.- A curried producer whose recipe annotates its draft and returns a new state, such as
create((draft: State) => ({ ...draft, count: 0 })), returnsState, orPromise<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 increate((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>, givesState | Promise<State>, also for a primitive state type, such asnumber, and in curried producers and creators frommakeCreator(). v1 typed the result asState.
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,deleteandcleartake 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'ssetalready had to in v1, a Set subclass'saddmust store the value that it receives: anaddthat 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
undefinedto a key thatdelete,shift,unshiftor a shrinkingspliceremoved from a draft adds the key back; v1 left a hole or kept the shorter length. - Under a
markthat returnsmutable, a value that the recipe assigned or moved is read back as assigned, through the draft and incurrent(); 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 thatmarkmakes 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'skeys()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, andkeys()continues to return a native iterator. apply()copies the own symbol keys of patch values, and an own__proto__key, asJSON.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. Withmutable: 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 as1or'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__reachArray.prototype. apply()withmutable: truethrows 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
markmakes draftable, such as a plain object undermarkSimpleObject, keeps an own__proto__key, asJSON.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
0and-0, which the state already kept; in v1, applying the patches lost the sign. An element that staysNaNno longer yields a replace patch. - When a recipe shortens an array and lengthens it again, as
draft.length = 1; draft.length = 3does, 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
thenmethod, and preserves the original error. - A draft of an array whose
Symbol.isConcatSpreadableis false copies its elements; v1 put the whole array into the copy as its only element.
Edit on GitHub
Last updated on by unadlib