Giter Site home page Giter Site 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.71% CSS 0.38% MDX 0.41%
immer immutability immutable reducer redux mutable mutation state-management mutative react

mutative's Introduction

Mutative

Mutative Logo

Node CI Coverage Status npm NPM Downloads license

Mutative - A JavaScript library for efficient immutable updates, 2-6x faster than naive handcrafted reducer, and more than 10x faster than Immer.

Why is Mutative faster than the spread operation(naive handcrafted reducer)?

The spread operation has performance pitfalls, which can be detailed in the following article:

And Mutative optimization focus on shallow copy optimization, more complete lazy drafts, finalization process optimization, and more.

Motivation

Writing immutable updates by hand is usually difficult, prone to errors, and cumbersome. Immer helps us write simpler immutable updates with "mutative" logic.

But its performance issue causes a runtime performance overhead. Immer must have auto-freeze enabled by default(Performance will be worse if auto-freeze is disabled), such immutable state with Immer is not common. In scenarios such as cross-processing, remote data transfer, etc., these immutable data must be constantly frozen.

There are more parts that could be improved, such as better type inference, non-intrusive markup, support for more types of immutability, Safer immutability, more edge cases, and so on.

This is why Mutative was created.

Mutative vs Naive Handcrafted Reducer Performance

Mutative vs Reducer benchmark by object:
  • Naive handcrafted reducer
// baseState type: Record<string, { value: number }>
const state = {
  ...baseState,
  key0: {
    ...baseState.key0,
    value: i,
  },
};
  • Mutative
const state = create(baseState, (draft) => {
  draft.key0.value = i;
});

Mutative vs Reducer benchmark by object

Measure(seconds) to update the 1K-100K items object, lower is better(view source).

Mutative is up to 2x faster than naive handcrafted reducer for updating immutable objects.

Mutative vs Reducer benchmark by array:
  • Naive handcrafted reducer
// baseState type: { value: number }[]

// slower 6x than Mutative
const state = [
  { ...baseState[0], value: i },
  ...baseState.slice(1, baseState.length),
];

// slower 2.5x than Mutative
// const state = baseState.map((item, index) =>
//   index === 0 ? { ...item, value: i } : item
// );

// same performance as Mutative
// const state = [...baseState];
// state[0] = { ...baseState[0], value: i };

The actual difference depends on which spread operation syntax you use.

  • Mutative
const state = create(baseState, (draft) => {
  draft[0].value = i;
});

Mutative vs Reducer benchmark by array

Measure(seconds) to update the 1K-100K items array, lower is better(view source).

Mutative is up to 6x faster than naive handcrafted reducer for updating immutable arrays.

Mutative vs Immer Performance

Mutative passed all of Immer's test cases.

Measure(ops/sec) to update 50K arrays and 1K objects, bigger is better(view source). [Mutative v1.0.5 vs Immer v10.1.1]

Benchmark

Naive handcrafted reducer - No Freeze x 4,442 ops/sec ±0.38% (95 runs sampled)
Mutative - No Freeze x 6,199 ops/sec ±0.79% (89 runs sampled)
Immer - No Freeze x 5.30 ops/sec ±0.38% (18 runs sampled)

Mutative - Freeze x 974 ops/sec ±1.77% (92 runs sampled)
Immer - Freeze x 376 ops/sec ±0.67% (92 runs sampled)

Mutative - Patches and No Freeze x 969 ops/sec ±0.99% (97 runs sampled)
Immer - Patches and No Freeze x 5.27 ops/sec ±0.36% (18 runs sampled)

Mutative - Patches and Freeze x 514 ops/sec ±0.97% (95 runs sampled)
Immer - Patches and Freeze x 275 ops/sec ±0.74% (89 runs sampled)

The fastest method is Mutative - No Freeze

Run yarn benchmark to measure performance.

OS: macOS 14.2.1, CPU: Apple M1 Max, Node.js: v20.11.0

Immer relies on auto-freeze to be enabled, if auto-freeze is disabled, Immer will have a huge performance drop and Mutative will have a huge performance lead, especially with large data structures it will have a performance lead of more than 50x.

So if you are using Immer, you will have to enable auto-freeze for performance. Mutative is disabled auto-freeze by default. With the default configuration of both, we can see the 16x performance gap between Mutative (6,199 ops/sec) and Immer (376 ops/sec).

Overall, Mutative has a huge performance lead over Immer in more performance testing scenarios. Run yarn performance to get all the performance results locally.

More Performance Testing Scenarios, Mutative is up to `2.5X-73.8X` faster than Immer:

Mutative vs Immer - All benchmark results by average multiplier

view source.

Features and Benefits

  • Mutation makes immutable updates - Immutable data structures supporting objects, arrays, Sets and Maps.
  • High performance - 10x faster than immer by default, even faster than naive handcrafted reducer.
  • Optional freezing state - No freezing of immutable data by default.
  • Support for JSON Patch - Full compliance with JSON Patch specification.
  • Custom shallow copy - Support for more types of immutable data.
  • Support mark for immutable and mutable data - Allows for non-invasive marking.
  • Safer mutable data access in strict mode - It brings more secure immutable updates.
  • Support for reducer - Support reducer function and any other immutable state library.

Difference between Mutative and Immer

Mutative Immer
Custom shallow copy
Strict mode
No data freeze by default
Non-invasive marking
Complete freeze data
Non-global config
async draft function
Fully compatible with JSON Patch spec

Mutative has fewer bugs such as accidental draft escapes than Immer, view details.

Installation

Yarn

yarn add mutative

NPM

npm install mutative

CDN

  • Unpkg: <script src="https://unpkg.com/mutative"></script>
  • JSDelivr: <script src="https://cdn.jsdelivr.net/npm/mutative"></script>

Usage

import { create } from 'mutative';

const baseState = {
  foo: 'bar',
  list: [{ text: 'coding' }],
};

const state = create(baseState, (draft) => {
  draft.list.push({ text: 'learning' });
});

expect(state).not.toBe(baseState);
expect(state.list).not.toBe(baseState.list);

create(baseState, (draft) => void, options?: Options): newState

The first argument of create() is the base state. Mutative drafts it and passes it to the arguments of the draft function, and performs the draft mutation until the draft function finishes, then Mutative will finalize it and produce the new state.

Use create() for more advanced features by setting options.

APIs

create()

Use create() for draft mutation to get a new state, which also supports currying.

import { create } from 'mutative';

const baseState = {
  foo: 'bar',
  list: [{ text: 'todo' }],
};

const state = create(baseState, (draft) => {
  draft.foo = 'foobar';
  draft.list.push({ text: 'learning' });
});

In this basic example, the changes to the draft are 'mutative' within the draft callback, and create() is finally executed with a new immutable state.

create(state, fn, options)

Then options is optional.

  • strict - boolean, the default is false.

    Forbid accessing non-draftable values in strict mode(unless using unsafe()).

    When strict mode is enabled, mutable data can only be accessed using unsafe().

    It is recommended to enable strict in development mode and disable strict in production mode. This will ensure safe explicit returns and also keep good performance in the production build. If the value that does not mix any current draft or is undefined is returned, then use rawReturn().

  • enablePatches - boolean | { pathAsArray?: boolean; arrayLengthAssignment?: boolean; }, the default is false.

    Enable patch, and return the patches/inversePatches.

    If you need to set the shape of the generated patch in more detail, then you can set pathAsArray and arrayLengthAssignmentpathAsArray default value is true, if it's true, the path will be an array, otherwise it is a string; arrayLengthAssignment default value is true, if it's true, the array length will be included in the patches, otherwise no include array length(NOTE: If arrayLengthAssignment is false, it is fully compatible with JSON Patch spec, but it may have additional performance loss), view related discussions.

  • enableAutoFreeze - boolean, the default is false.

    Enable autoFreeze, and return frozen state, and enable circular reference checking only in development mode.

  • mark - (target) => ('mutable'|'immutable'|function) | (target) => ('mutable'|'immutable'|function)[]

    Set a mark to determine if the value is mutable or if an instance is an immutable, and it can also return a shallow copy function(AutoFreeze and Patches should both be disabled, Some patches operation might not be equivalent). When the mark function is (target) => 'immutable', it means all the objects in the state structure are immutable. In this specific case, you can totally turn on AutoFreeze and Patches. mark supports multiple marks, and the marks are executed in order, and the first mark that returns a value will be used. When a object tree node is marked by the mark function as mutable, all of its child nodes will also not be drafted by Mutative and will retain their original values.

create() - Currying

  • create draft
const [draft, finalize] = create(baseState);
draft.foobar.bar = 'baz';
const state = finalize();

Support set options such as const [draft, finalize] = create(baseState, { enableAutoFreeze: true });

  • create producer
const produce = create((draft) => {
  draft.foobar.bar = 'baz';
});
const state = produce(baseState);

Also support set options such as const produce = create((draft) => {}, { enableAutoFreeze: true });

apply()

Use apply() for applying patches to get the new state.

import { create, apply } from 'mutative';

const baseState = {
  foo: 'bar',
  list: [{ text: 'todo' }],
};

const [state, patches, inversePatches] = create(
  baseState,
  (draft) => {
    draft.foo = 'foobar';
    draft.list.push({ text: 'learning' });
  },
  {
    enablePatches: true,
  }
);

const nextState = apply(baseState, patches);
expect(nextState).toEqual(state);
const prevState = apply(state, inversePatches);
expect(prevState).toEqual(baseState);

current()

Get the current value from a draft.

  • For any draft where a child node has been modified, the state obtained by executing current() each time will be a new reference object.
  • For a draft where no child nodes have been modified, executing current() will always return the original state.

It is recommended to minimize the number of times current() is executed when performing read-only operations, ideally executing it only once.

const state = create({ a: { b: { c: 1 } }, d: { f: 1 } }, (draft) => {
  draft.a.b.c = 2;
  expect(current(draft.a)).toEqual({ b: { c: 2 } });
  // The node `a` has been modified.
  expect(current(draft.a) === current(draft.a)).toBeFalsy();
  // The node `d` has not been modified.
  expect(current(draft.d) === current(draft.d)).toBeTruthy();
});

original()

Get the original value from a draft.

const baseState = {
  foo: 'bar',
  list: [{ text: 'todo' }],
};

const state = create(baseState, (draft) => {
  draft.foo = 'foobar';
  draft.list.push({ text: 'learning' });
  expect(original(draft.list)).toEqual([{ text: 'todo' }]);
});

unsafe()

When strict mode is enabled, mutable data can only be accessed using unsafe().

const baseState = {
  list: [],
  date: new Date(),
};

const state = create(
  baseState,
  (draft) => {
    unsafe(() => {
      draft.date.setFullYear(2000);
    });
    // or return the mutable data:
    // const date = unsafe(() => draft.date);
  },
  {
    strict: true,
  }
);

If you'd like to enable strict mode by default in a development build and turn it off for production, you can use strict: process.env.NODE_ENV !== 'production'.

isDraft()

Check if a value is a draft.

const baseState = {
  date: new Date(),
  list: [{ text: 'todo' }],
};

const state = create(baseState, (draft) => {
  expect(isDraft(draft.date)).toBeFalsy();
  expect(isDraft(draft.list)).toBeTruthy();
});

isDraftable()

Check if a value is draftable

const baseState = {
  date: new Date(),
  list: [{ text: 'todo' }],
};

expect(isDraftable(baseState.date)).toBeFalsy();
expect(isDraftable(baseState.list)).toBeTruthy();

You can set a mark to determine if the value is draftable, and the mark function should be the same as passing in create() mark option.

rawReturn()

For return values that do not contain any drafts, you can use rawReturn() to wrap this return value to improve performance. It ensure that the return value is only returned explicitly.

const baseState = { id: 'test' };
const state = create(baseState as { id: string } | undefined, (draft) => {
  return rawReturn(undefined);
});
expect(state).toBe(undefined);

If the return value mixes drafts, you should not use rawReturn().

const baseState = { a: 1, b: { c: 1 } };
const state = create(baseState, (draft) => {
  if (draft.b.c === 1) {
    return {
      ...draft,
      a: 2,
    };
  }
});
expect(state).toEqual({ a: 2, b: { c: 1 } });
expect(isDraft(state.b)).toBeFalsy();

If you use rawReturn(), we recommend that you enable strict mode in development.

const baseState = { a: 1, b: { c: 1 } };
const state = create(
  baseState,
  (draft) => {
    if (draft.b.c === 1) {
      return rawReturn({
        ...draft,
        a: 2,
      });
    }
  },
  {
    strict: true,
  }
);
// it will warn `The return value contains drafts, please don't use 'rawReturn()' to wrap the return value.` in strict mode.
expect(state).toEqual({ a: 2, b: { c: 1 } });
expect(isDraft(state.b)).toBeFalsy();

makeCreator()

makeCreator() only takes options as the first argument, resulting in a custom create() function.

const baseState = {
  foo: {
    bar: 'str',
  },
};

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

const [state, patches, inversePatches] = create(baseState, (draft) => {
  draft.foo.bar = 'new str';
});

markSimpleObject()

markSimpleObject() is a mark function that marks all objects as immutable.

const baseState = {
  foo: {
    bar: 'str',
  },
  simpleObject: Object.create(null),
};

const state = create(
  baseState,
  (draft) => {
    draft.foo.bar = 'new str';
    draft.simpleObject.a = 'a';
  },
  {
    mark: markSimpleObject,
  }
);

expect(state.simpleObject).not.toBe(baseState.simpleObject);

View more API docs.

Using TypeScript

  • castDraft()
  • castImmutable()
  • Draft<T>
  • Immutable<T>
  • Patches
  • Patch
  • Options<O, F>

Integration with React

  • use-mutative - A 2-6x faster alternative to useState with spread operation
  • use-travel - A React hook for state time travel with undo, redo, reset and archive functionalities.
  • zustand-mutative - A Mutative middleware for Zustand enhances the efficiency of immutable state updates.

FAQs

  • I'm already using Immer, can I migrate smoothly to Mutative?

Yes. Unless you have to be compatible with Internet Explorer, Mutative supports almost all of Immer features, and you can easily migrate from Immer to Mutative.

Migration is also not possible for React Native that does not support Proxy. React Native uses a new JS engine during refactoring - Hermes, and it (if < v0.59 or when using the Hermes engine on React Native < v0.64) does not support Proxy on Android, but React Native v0.64 with the Hermes engine support Proxy.

  • Can Mutative be integrated with Redux?

Yes. Mutative supports return values for reducer, and redux-toolkit is considering support for configurable produce().

Migration from Immer to Mutative

mutative-compat - Mutative wrapper with full Immer API compatibility, you can use it to quickly migrate from Immer to Mutative.

  1. produce() -> create()

Mutative auto freezing option is disabled by default, Immer auto freezing option is enabled by default (if disabled, Immer performance will have a more huge drop).

You need to check if auto freezing has any impact on your project. If it depends on auto freezing, you can enable it yourself in Mutative.

import produce from 'immer';

const nextState = produce(baseState, (draft) => {
  draft[1].done = true;
  draft.push({ title: 'something' });
});

Use Mutative

import { create } from 'mutative';

const nextState = create(baseState, (draft) => {
  draft[1].done = true;
  draft.push({ title: 'something' });
});
  1. Patches
import { produceWithPatches, applyPatches } from 'immer';

enablePatches();

const baseState = {
  age: 33,
};

const [nextState, patches, inversePatches] = produceWithPatches(
  baseState,
  (draft) => {
    draft.age++;
  }
);

const state = applyPatches(nextState, inversePatches);

expect(state).toEqual(baseState);

Use Mutative

import { create, apply } from 'mutative';

const baseState = {
  age: 33,
};

const [nextState, patches, inversePatches] = create(
  baseState,
  (draft) => {
    draft.age++;
  },
  {
    enablePatches: true,
  }
);

const state = apply(nextState, inversePatches);

expect(state).toEqual(baseState);
  1. Return undefined
import produce, { nothing } from 'immer';

const nextState = produce(baseState, (draft) => {
  return nothing;
});

Use Mutative

import { create, rawReturn } from 'mutative';

const nextState = create(baseState, (draft) => {
  return rawReturn(undefined);
});

Contributing

Mutative goal is to provide efficient and immutable updates. The focus is on performance improvements and providing better APIs for better development experiences. We are still working on it and welcome PRs that may help Mutative.

Development Workflow:

  • Clone Mutative repo.
  • Run yarn install to install all the dependencies.
  • Run yarn prettier to format the code.
  • yarn test --watch runs an interactive test watcher.
  • Run yarn commit to make a git commit.

License

Mutative is MIT licensed.

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

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

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.

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`

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

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.

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.

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.

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)

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

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.

[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: 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.

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?

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?

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

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!

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

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

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.

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?

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.