Coder Social home page Coder Social logo

unadlib / mutative Goto Github PK

View Code? Open in Web Editor NEW
1.5K 14.0 13.0 13.54 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.45% JavaScript 1.75% CSS 0.39% MDX 0.42%
immer immutability immutable reducer redux mutable mutation state-management mutative react

mutative's People

Contributors

exuanbo 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  avatar

mutative's Issues

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.

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.

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.

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

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.

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.

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?

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.

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

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?

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?

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)

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

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;

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

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);

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?

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.

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.