unadlib / mutative Goto Github PK
View Code? Open in Web Editor NEWEfficient immutable updates, 2-6x faster than naive handcrafted reducer, and more than 10x faster than Immer.
Home Page: http://mutative.js.org/
License: MIT License
Efficient immutable updates, 2-6x faster than naive handcrafted reducer, and more than 10x faster than Immer.
Home Page: http://mutative.js.org/
License: MIT License
This is a proposal https://github.com/tc39/proposal-set-methods to add methods like union and intersection to JavaScript's built-in Set class.
It is currently at stage 4: it has been tc39/ecma262#3306. This repository is no longer active.
This would add the following methods:
With the support of the latest major browsers, it is necessary for Mutative to support it as well.
To make the migration from Immer to Mutative smoother, we consider return values that are all traversed and finalized by default. For return values without mixed drafts, rawReturn()
can be used to improve performance, and we will hint at such an optimization with checking mixed drafts in strict mode.
Hello.
First of all thank you for the library, it works really great!
I've just bumped into one issue. In our setup we have iframe & parent window running on same domain and interacting with one another.
One of those interactions is:
const intialState = iframe.someFunction();
const state = create(initialState, ...);
Due to this, even though the returned object is "simple", the Object.getPrototypeOf(value) === Object.prototype
check fails.
I can overcome this by using mark functionality:
const state = create(initialState, ..., { mark: () => "immutable" });
This brings a couple of questions:
mark: () => "immutable",
? Will this lead to some side effects? I haven't digged too deep into the source code yet, but couldn't find info in the docs.mark
docs, it says that (AutoFreeze and Patches should both be disabled). Is that really the case? I've tried to use them with mark
and it looked like it works fine.Mutative and Immer both draft class instances. Despite Mutative's speed advantage over Immer, it still has considerable scope for enhancing performance in shallow copying of class instances.
As part of an SDK I'm working on I provide a draft version of user provided data structure back to them to be updated. I want to maintain the immutable status of the original data so that I can compare it safely later on.
However, in this case the user has provided a reasonably large data structure that represents a physics engine. The performance of making changes to the mutative draft is understandably not as fast as a raw object - however, the performance of reads of properties of the draft seem to be impacted too.
To test this out I've put together a simple standalone test case over here: https://github.com/kevglass/mutative-performance-sample/ - it's important to note that the create() is intentionally inside the loop since in the real system the draft is created every frame.
It simulates a collection of balls (30) on a table moving in random directions and colliding. The performance test can be run in two ways - either with writing to the draft object (the same as it would be in a real physics engine) or in read only mode where the simulation is just calculating some values based on the contents of the draft objects.
I feel like I must be doing something wrong but I can't quite understand what it is. The results on my M1 for read only access to the draft object looks like this:
2024-03-11T21:23:43.254Z
Iterations=5000 Balls=30 ReadOnly=true
RAW : 5000 iterations @12ms (0.0024 per loop)
RAW+COPY: 5000 iterations @254ms (0.0508 per loop)
MUTATIVE: 5000 iterations @3709ms (0.7418 per loop)
IMMER : 5000 iterations @4309ms (0.8618 per loop)
Where RAW is a simple JS object, RAW+COPY is taking a copy of the object at each stage (parse/stringify). Mutative is the lovely library here and Immer for comparison.
I hadn't expected the impact of reading from the draft to be so high, so i'm guessing I've done something very wrong.
Any thoughts or directions appreciated.
For completeness heres my read/write results from my M1:
RAW : 5000 iterations @14ms (0.0028 per loop)
RAW+COPY: 5000 iterations @270ms (0.054 per loop)
MUTATIVE: 5000 iterations @4813ms (0.9626 per loop)
IMMER : 5000 iterations @5430ms (1.086 per loop)
Mutative v0.3.2 does not fully support the JSON Patch spec.
path
type is an array, not a string as defined in the JSON patch spec.length
, which is not consistent with JSON Patch spec.Since standard JSON patches are often used to sync backend with front-end state, compliance with JSON patch standard is necessary. However, considering array clearing patches will bring predictable performance loss, and has path
type conversion issues.
We propose to add the option usePatches: 'json-patch' | 'never' | 'always'
, with the default value of never
, and remove enablePatches
.
usePatches
is always
, the patches it produces will not exactly match JSON patch spec, but it will maintain the good performance that most uses of patches require.usePatches
is json-patch
, it produces patches that will be fully compliant with JSON patch spec, which has a slight performance penalty, and it ensures that the data it produces can be passed to other backend APIs based on JSON patch spec.Hello. Thank you for quick replies on previous issues! We've noticed one more issue:
import { create } from 'mutative';
const baseState = {
array: [
{x: 1}
]
};
const state = create(baseState, (draft) => {
draft.array = draft.array.filter(o => o.x !== 1)
});
console.log(state.array) // expected [], received [undefined]
This issue seems to happen if filtering is done on array that has objects inside, if it has numbers in it works correctly.
Sometimes I need to change type of original object. For example, I want to add new field, or change existing field. Currently it is impossible, either with mutative
, and with immer
both, as far as I know (maybe I am missing something?).
import { create } from 'mutative'
type S = {
x: string
}
type N = {
x: number
}
const x: S = {
x: "10"
}
// Type 'S' is not assignable to type 'N'.
// Types of property 'x' are incompatible.
// Type 'string' is not assignable to type 'number'.(2322)
const y: N = create(x, draft => {
// Type 'number' is not assignable to type 'string'.(2322)
draft.x = Number(draft.x)
})
And without loosing type safety! I mean, I can write create(x as any, (draft) => { ... })
, but this is not nice.
I checked your tests, and you just shuts TypeScript up with @ts-ignore
in such cases.
Lines 1764 to 1765 in 3c2e66b
I don't know how to do it though... Maybe pass draft
object to mutation function twice? Actually it will be the same object, but you can say to TypeScript, that they are not. Something like this:
const y: N = create(x, (draft: S, resultDraft: N) => {
resultDraft.x = Number(draft.x)
})
Just an idea
I got a type error while getting the current value from draft using the current API
Argument of type 'string' is not assignable to parameter of type 'object'.ts(2345)
Is my usage incorrect? Or I don't need to wrap the string property with current.
What I tried to do
draft.b = current(draft.a);
draft.a = '3'; // <- This shouldn't change `draft.b`
Hi, thank you for this lib. I'm already using immer and wanted to try mutative out.
After replacing produce
with create
typescript is complaining (with immer I had no issues).
TS2769: No overload matches this call. The last overload gave the following error.
Type '(draft: { [key: string]: { file_name: string; state: string; type: string; uri: string; }; }) => void' has no properties in common with type 'Options<false, false>'.
Am I missing something? Thanks
Hello: My team is considering moving to mutative from immer. We would like to know below:
Thank you, again! The results so far.
I tried hiding a property in my baseState object by naming it with a Symbol
.
const SECRET = Symbol('__MY_SECRET__');
const baseState = { a: { name: "a" }, [SECRET]: "ignore-me" };
const [draft, finalize] = create(baseState, { enablePatches: true });
mutator(draft);
const [newState, patches] = finalize();
And this would work in most cases because the mutator would not have access to SECRET
.
However, I have a specific case where the mutator will have the symbol and will modify draft[SECRET]
. So I'm wondering if there is any way that I can get mutative to ignore a property of baseState
without mapping it onto a new object?
I stumbled upon this library because I was looking for something to do transactional updates to a mutable object tree, and sadly couldn't find anything.
The idea is simple, I have some mutable state that's deeply referenced throughout a piece of code. Now I want to run a DSL script on the state, but I don't trust the script to run to completion every time. So I want to buffer any updates to my state until it finishes running without crashing or being aborted by the user, and then commit the changes to my state without making a copy and having to update all references to it.
The API could be a mirror set of functions, eg. produceInPlace
, finalizeInPlace
, applyInPlace
.
Making sure the state isn't mutated from outside while a transaction is running is the caller's responsibility.
Hope that makes sense.
current
creates a copy of nested objects, even when such objects are not draft.
Consider the following example:
const obj = { k: 42 };
const original = { x: { y: { z: [obj] }}}
const yReplace = { z: [obj] } ;
// with create
const withCreate = create(original, draft => {
draft.x.y = yReplace;
});
console.log(withCreate.x.y === yReplace) // prints true
console.log(withCreate.x.y.z[0] === obj) // prints true
// with draft + current
const [draft] = create(original);
draft.x.y = yReplace;
const withDraft = current(draft);
console.log(withDraft.x.y === yReplace) // prints false
console.log(withDraft.x.y.z[0] === obj) // prints false! DEEP COPY???
I would expect the draft + current to behave like the create option, returning the new object, but currently actually performs a deep copy instead. This has a big negative impact on the performance of current
I have a use case where I access the base
state in an onSuccess
callback of a mutation. Something along the lines of
create((base) => {
// Make changes to base
mutation.mutate(..., { onSuccess: () => ctx.route.invalidate(base.id) })
})
However, it seems like base is a proxy object that gets revoked at some point between the call to mutation.mutate()
and the call to onSuccess()
. Currently I'm copying the base into a separate object before every mutation like this
create((base) => {
// Make changes to base
base = { ...base }
mutation.mutate(..., { onSuccess: () => ctx.route.invalidate(base.id) })
})
but that is not ideal.
Is mutative actually passing a proxy object as base
? If so, is this addressable on your end and/or are there any other workarounds I could use?
Note: this issue does not occur if running React in Strict Mode.
If support return values in the draft function, this would mean that Mutative would need to determine if the value is a draft, and do a deep traversal of non-draft return values, and such an unpredictable return value would waste a considerable amount of performance.
But the community wants mutative to support it. The good thing is that Mutative makes a lot of performance improvements, and it doesn't lose any performance as long as we suggest trying to return the draft itself as much as possible in usage scenarios like Redux's reducer. Even if the returned value is not a draft, it doesn't have significant performance loss and we will have performance benchmarks to track it.
Hello.
I've noticed part of user written code that used to work with immer does not work with mutative. Here is code snippet:
import { create } from 'mutative';
import { produce } from 'immer';
const baseState = {
array: [
{
one: {
two: 3,
},
}
]
};
const created = create(baseState, (draft) => {
draft.array[0].one.two = 2
draft.array = [draft.array[0]]
});
created.array[0].one.two // 3
const produced = produce(baseState, (draft) => {
draft.array[0].one.two = 2
draft.array = [draft.array[0]]
});
produced.array[0].one.two // 2
Curious to hear is this expected?
mark
option is used to customize either mutable or immutable data. To make it pluggable, we might consider allowing it to support multiple functionalities.
For example,
const immutable = Symbol.for("immutable");
const mutableMark = (target, types) => {
if (target[immutable]) {
return types.immutable;
}
};
const state = create(
data,
(draft) => {
draft.foobar.text = "new text";
},
{
mark: [
mutableMark,
(target, { mutable }) => {
if (target === data.foobar) return mutable;
}
],
}
);
makeCreator()
only takes options as the first argument, resulting in a create
function. This function can take either the current immutable data or a draft function as an argument, or it can take both as arguments.
const baseState = {
foo: {
bar: 'str',
},
};
const create = makeCreator({
enablePatches: true,
});
const [state, patches, inversePatches] = create(baseState, (draft) => {
draft.foo.bar = 'new str';
});
const baseState = {
foo: {
bar: 'str',
},
};
const create = makeCreator({
enablePatches: true,
});
const [draft, finalize] = create(baseState);
draft.foo.bar = 'new str';
const [state, patches, inversePatches] = finalize();
const baseState = {
foo: {
bar: 'str',
},
};
const create = makeCreator({
enablePatches: true,
});
const generate = create((draft) => {
draft.foo.bar = 'new str';
});
const [state, patches, inversePatches] = generate(baseState);
After the draft function is executed, if the draft tree has not really changed its values, it should return to its original state.
Although Mutative and Immer behave the same behavior, we are considering supporting new behavior, as it can reduce some unexpected shallow comparison performance due to changed states(serializes to the same string).
For example,
const baseState = { a: { b: 1 } };
const state = produce(baseState, (draft) => {
delete draft.a.b;
draft.a.b = 1;
});
expect(state).not.toBe(baseState); // They should be equal.
Hello!
There is a new bug in the current coming from this PR that actually changes the original objects unexpectedly, breaking:
Here are two failing tests:
it("doesn't assign values to frozen object", () => {
const frozen = Object.freeze({ test: 42 })
const base = { k: null };
produce(base, (draft) => {
draft.k = frozen;
const c = current(draft); // Boom! tries to set a value in the frozen object
expect(c.k).toBe(frozen);
});
});
it("nested drafts work after current", () => {
const base = { k1: {}, k2: {} };
const result = produce(base, (draft) => {
const obj = { x: draft.k2 };
draft.k1 = obj;
current(draft); // try to comment me
obj.x.abc = 100;
draft.k2.def = 200;
});
expect(result).toEqual({ k1: { x: { abc: 100, def: 200 }}, k2: { abc: 100, def: 200 } });
});
The first should be solvable by not assigning the value to the parent if the getCurrent
(child value) has same identity as the child value (or if the object is frozen).
For the second one, I don't see ways of escaping the shallow copy.
In README:
Migration is also not possible for React Native which does not support Proxy.
Doesn't react-native support proxy in 0.64? See https://reactnative.dev/blog/2021/03/12/version-0.64#hermes-with-proxy-support.
I noticed a very small issue where tests using new Date
and setFullYear
don't pass locally for me, I believe due to timezone issues.
I made a quick PR: #40
The basic syntax is the same, so when changing from immer to mutative, it is only necessary to replace the import statement.
I haven't looked at the source code, but I think it would be as simple as adding the following one sentence.
export const produce = create;
Thank you for creating this awesome project! I have been working on a library that implements the full immer
API using mutative
under the hood (why not 😄) and I noticed some differences. I'm not sure if this is intended or not. If you think this is a bug, I'm happy to create a PR to fix it.
import { create } from 'mutative';
test('object with Symbol key at root', () => {
const a = Symbol('a');
const data: Record<PropertyKey, any> = {
[a]: 'str',
};
const state = create(data, (draft) => {
expect(draft[a]).toBe('str');
draft.foobar = 'str';
});
expect(state).toEqual({
[a]: 'str',
foobar: 'str',
});
});
StackBlitz: https://stackblitz.com/edit/vitest-dev-vitest-4fcbnc?file=test%2Fcreate.test.ts
This will fail with:
- Expected
+ Received
Object {
"foobar": "str",
- Symbol(a): "str",
}
The symbol key is not copied from the original object.
And I also found this test case:
mutative/test/immer/__tests__/base.ts
Lines 908 to 921 in 5e567cd
Using create
to apply a reducer on some Class instance marked as immutable
and then comparing the result data to some expected class instance with chai's deep equality util throws a Proxy revoked error:
// Where `src` is some non trivial class instance
const data = create(src, (draft) => {}, { mark: () => "immutable" });
expect(data).to.eql(expected); // Cannot perform 'ownKeys' on a proxy that has been revoked
Do you know why?
Feeding class instances to create function causes the following Error, unless they are wrapped with a primitive like it's done in the class benchmark example.
Error: Invalid base state: create() only supports plain objects, arrays, Set, Map or using mark() to mark the state as immutable
Is this by design? What is the recommended usage pattern for mutating class instances?
Regarding return values, Mutative has the same behavior as Immer.
An draft function returned a new value and modified its draft. Either return a new value or modify the draft.
For example,
expect(() => {
const state = create({ a: 1 }, (draft) => {
draft.a = 2;
return {
...draft,
};
});
}).toThrowError();
However, there is an irrational aspect to this approach. As long as a modified draft can be finalized, it should be allowed to return any value.
Therefore, we are considering allowing Mutative to support draft functions returning any value.
Hey there, I hope this is an okay place to ask this question.
I was wondering if there was any way to by default enable strict mode globally? I'd like to enable strict mode by default in a development build, and turn it off for production, but haven't found an ergonomic way to do that.
In your docs this is also the recommended way:
Would love to hear suggestions for users of this library on how they would go about doing that.
Thanks in advance!
Mutative has fixed some edge cases, as well as support for reducers, and full support for the JSON Patch spec. Mutative will not consider supporting circular references and IE browser.
Do you have any comments or suggestions about Mutative official v1?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.