GithubHelp home page GithubHelp logo

unadlib / mutative Goto Github PK

View Code? Open in Web Editor NEW
1.6K 13.0 18.0 13.98 MB

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

TypeScript 97.50% JavaScript 1.72% CSS 0.38% MDX 0.41%
immer immutability immutable reducer redux mutable mutation state-management mutative react

mutative's People

Contributors

dabbott avatar exuanbo avatar francescotescari avatar unadlib avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

mutative's Issues

Enhancements to the proposal Set methods

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:

  • Set.prototype.intersection(other)
  • Set.prototype.union(other)
  • Set.prototype.difference(other)
  • Set.prototype.symmetricDifference(other)
  • Set.prototype.isSubsetOf(other)
  • Set.prototype.isSupersetOf(other)
  • Set.prototype.isDisjointFrom(other)

With the support of the latest major browsers, it is necessary for Mutative to support it as well.

Check compatibility

Simple object check

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:

  1. Is it okay to simply use 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.
  2. Next to 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.
  3. Maybe there is a different kind of "simple" object check that would not break in this crazy scenario?

Performance of Reads on Draft

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)

Proposal: full support for JSON Patch spec

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.
  • The patches generated by Mutative array clearing are a modification of the array length, which is not consistent with JSON Patch spec.
  • Need to support JSON Pointer 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.

  • If the option 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.
  • If the option 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.

Filter does not work correctly when array contains objects

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.

Need a nice handy way to opt return type

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.

mutative/test/create.test.ts

Lines 1764 to 1765 in 3c2e66b

// @ts-ignore
draft.x = a;

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

Using current api with string

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`

TS2769: No overload matches this call on draft

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>'.

Screenshot 2023-01-02 at 12 28 30

Am I missing something? Thanks

Difference between mutative and immer

Hello: My team is considering moving to mutative from immer. We would like to know below:

  1. Main difference in implementation of state production in mutative than immer, which causes mutative to be faster. For example, how does mutative handle deeply nested states, et al?
  2. Test coverage for mutative.
  3. Production ready date or when V1 would be released.

Thank you, again! The results so far.

Is there a way to make mutative ignore a property?

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?

Proposal: support in-place updates of original mutable state

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.

performance: current creates new copies of the objects where unnecessary

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

TypeError: Cannot perform 'get' on a proxy that has been revoked

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.

Proposal: support return values in the draft function

reduxjs/redux-toolkit#3074

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.

Modification inside create is lost and differs from immer

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?

Proposal: support multiple mark function

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;
      }
    ],
  }
);

Proposal: support currying such makeCreator(options)

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.

  • Take the current immutable data and a draft function as arguments:
const baseState = {
  foo: {
    bar: 'str',
  },
};

const create = makeCreator({
  enablePatches: true,
});

const [state, patches, inversePatches] = create(baseState, (draft) => {
  draft.foo.bar = 'new str';
});
  • Just take the current immutable data as an argument:
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();
  • Just take a draft function as an argument:
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);

Proposal: Support modification and restore to the original value.

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.

current mutates the original objects unexpectedly

Hello!
There is a new bug in the current coming from this PR that actually changes the original objects unexpectedly, breaking:

  • Use of frozen objects in the draft (this actually makes my app crash)
  • Weird behavior when using nested drafts.

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.

Proposal: add produce, which is an ALIAS of create.

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;

[Question] behavior with symbol keys

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:

it('preserves symbol properties', () => {
const test = Symbol('test');
const baseState = { [test]: true };
const nextState = produce(baseState, (s) => {
// !!! This is different from immer
// expect(s[test]).toBeTruthy();
s.foo = true;
});
// !!! This is different from immer
expect(nextState).toEqual({
// [test]: true,
foo: true,
});
});

Proxy revoked error when performing chai deep equality

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?

Is it possible to use class instances with create?

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?

Proposal: Support draft function return value with modified draft

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.

Ability to make Strict Mode the default

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:
image

Would love to hear suggestions for users of this library on how they would go about doing that.

Thanks in advance!

Plan: production-ready version coming soon

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?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.