Coder Social home page Coder Social logo

immerjs / immer Goto Github PK

View Code? Open in Web Editor NEW
27.0K 156.0 836.0 6.08 MB

Create the next immutable state by mutating the current one

Home Page: https://immerjs.github.io/immer/

License: MIT License

JavaScript 38.82% TypeScript 27.10% HTML 10.73% CSS 0.44% MDX 22.92%
immutable immutables reducer redux state-tree

immer's People

Contributors

aleclarson avatar arthurdenner avatar benbraou avatar cdauth avatar dalcib avatar dependabot[bot] avatar gregjarvez avatar hrsh7th avatar iruca3 avatar jkhedani avatar kachkaev avatar knpwrs avatar kof avatar markerikson avatar mweststrate avatar mylesj avatar nifgraup avatar orisomething avatar pekeler avatar phryneas avatar pkerschbaum avatar pvcresin avatar rdil avatar runnez avatar shuumatsu avatar slikts avatar smirea avatar styfle avatar tomfaulkner avatar yanick 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  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

immer's Issues

Introduce `create(Immer)Reducer` utility?

the pattern is now typically:

function insertItem(array, action) {
    return immer(array, draft => {
        /* user code */ draft.splice(action.index, 0, action.item)
    })
}

So we could introduce a utility

createImmerReducer(fn: (draftState, action) => {})

which allows writing

const insertItem = createImmerReducer((draft, action) => {
    /* user code */ draft.splice(action.index, 0, action.item)
})

The implementation would roughly be:

function createImmerReducer(fn) {
  return (state, action) => immer(state, draft => fn(draft, action))
}

implementation comparison

I made similar library (also auto-immutability via Proxy) but my library takes quite a different approach to implementation.
https://github.com/hex13/enter-ghost/tree/master/packages/transmutable

I don’t create copies in set traps. Instead of that I only “record” mutations like they were events (I’ve got an array with list of “what happened”, e.g.

state.foo.bar = 3
state.baz = 4

get recorded as

[
  {type: 'set', path: ['foo', 'bar'], value: 3},
  {type: 'set', path: ['baz'], value: 4}
]

Then (after recording all mutations) I make a deep clone of the whole state tree (but I do dirty checking, based on recorded mutations - if something is not changed in mutation, it's copied by reference).

And then I replay recorded mutations and apply them to the clone of state.

On the other hand I've noticed that you took a different approach to implementation. From what I've seen you're making ad-hoc copies of state in Proxy and change them immediately (without recording mutations).

Which approach is better?

I think both have pros and cons.

Event-sourcing approach allows for recording all mutations. Probably in Redux setting it won't be
needed (because Redux is already kind of event-sourced) but I was experimenting with making my own state management library and event-sourcing allowed me to make such a nice dev tools for it which showed me which exact mutations occured: https://hex13.github.io/demos/todo/

Event-sourcing approach also can help with the versioning objects (undo, redo, clone, merge) but with Redux it could probably be done in some different way.

on the other hand because my implementation has two phases (1. record mutations 2. make a copy and replay mutations) and this has some shortcomings. Because it applies all changes all at once, this means state in transformer always point to old, stale version of state:

transmutable

transform({arr: [10,20]},  draft => {
    draft.arr.push(30);
    console.log(draft.arr); // [10, 20]
});

immer

immer({arr: [10,20]},  draft => {
    draft.arr.push(30);
    console.log(draft.arr); // [ 10, 20, <1 empty item> ]
});

This means that your approach (modify ad-hoc) feels more like VanillaJS. I make a push and something was pushed right away. Where in transmutable draft always returns values from old version.

I wonder if it was possible to merge these two approaches. I need event-sourcing* but your immediate approach has also clear advantages.

Besides yours and mine there are also similar libraries made by different people.
https://github.com/engineforce/ImmutableAssign
https://github.com/Palindrom/JSONPatcherProxy (I think this one only records mutations, without transforming).

*I'm intending to make full state management event-sourced library and transmutable is only a mean to an end.

Mobx

Did you thought about integrating immer into Mobx? (just curious). Observable that would modify themselves in immutable way. That could make Mobx immutable in simple way.

Support with redux-actions handleActions({})

@mweststrate - Can I use produce on my reducer with redux-action's handleActions(Object) method?

eg -

const reducer = (state = initState, action) => produce(state, draft => 
	handleActions({
			[ACTION_TYPES.FETCH_DATA]: (draft, action) => {
				draft.data = action.payload
			}
     })
)

minified version

I want to use minified file, but now immer doesn't have a minified version.

Issue while trying to use map, filter , find etc

I am trying out immer for a simple todos app. I have a simple reducer function like this -

Reducer( state = initialState, action) {
   let method = actionList[action.type];
        if(method){
		const nextState = produce(state, draft_state => {
			let new_state =  method( draft_state , action);
			return new_state
		})
		return nextState;
	}
	else return state
}

actionList is an object which functions as a clutter free version of typical reducer (using the above function in the hindsight):

const actionList = {
    ADD_TODO : (state, action) => state.push(action.todo),
    TOGGLE_TODO : (state, {todo}) => {
       return state.map( _todo =>{
           if( _todo.id==todo.id){
             _todo.completed = !_todo.completed
           }
        });
    }
}

state object goes like this:

const state =  [
        {
            id :1,
            text: 'sample todo',
            completed : true
        }
   ]

The issue is I am unable to get TOGGLE_TODO to work using this approach (or any approach). To put it simply, how may I toggle the completed flag of a todo using below method:

TOGGLE_TODO : (state, {todo}) => {
      // here state is the proxy of currentState generated by immer, todo is the todo object to be toggled
    }

here's sandbox link if that helps better:
https://codesandbox.io/s/ymw0127r39

Drop of performances on node 9

Hi,

I tried the performance benchmark on different versions of node and i experienced a weird drop of performances.

Here are my computer specifications:

CPU: 2,7 GHz Intel Core i5
RAM: 8 Go 1867 MHz DDR3

On 8.4.0:

  performance
    ✓ just mutate (2ms)
    ✓ deepclone, then mutate (475ms)
    ✓ handcrafted reducer (23ms)
    ✓ immutableJS (66ms)
    ✓ immer - with autofreeze (371ms)
    ✓ immer - without autofreeze (181ms)

On 9.3.0:

  performance
    ✓ just mutate (5ms)
    ✓ deepclone, then mutate (324ms)
    ✓ handcrafted reducer (19ms)
    ✓ immutableJS (69ms)
    ✓ immer - with autofreeze (510ms)
    ✓ immer - without autofreeze (1378ms)

I didn't found out the cause of this.

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

Node: 8.4.0
immer: 0.5.0

const produce = require('immer').default;
const result = produce(
    {arr: [{count:1}, {count: 2}, {count: 3}]},
    draft => {
        draft.arr = draft.arr.filter(item => item.count > 2);
    }
);

console.log(result); // throws

result.arr[0].count seems to be troublesome, even referencing to this variable throws an error.

Result of function produce should be a plain JS object, right? Because it seems that the result sometimes contains proxies inside.

Does not seem to support changing the root object type.

Consider adding some warnings for this?

const produce = require('immer').default;

const result1 = produce([], draft => {
    draft = {}
})
console.log(result1) // []

const result2 = produce({}, draft => {
    draft = []
})
console.log(result2) // {}

Looking for feedback / testimonials

Hi all!

I'm looking for feedback. If you tried / are trying this package, how do you experience it? Does it deliver what it promises? Does it help reduce amount / complexity of code? Is there anything you are missing or that could be improved in this package or the docs?

Any kind of feedback is appreciated, thanks!

Can't log arrays (TypeError)

immer 0.4.1, Node v8.4.0

const produce = require('immer').default;
produce([],(arr) => { console.log(arr);  });
TypeError: 'getOwnPropertyDescriptor' on proxy: trap returned descriptor for property 'length' that is incompatible with the existing property in the proxy target
    at Function.keys (<anonymous>)
    at formatValue (util.js:392:21)
    at inspect (util.js:236:10)
    at format (util.js:97:24)
    at Console.log (console.js:113:24)
    at immer (repl:1:28)
    at produce (blah/node_modules/immer/immer.js:224:29)
    at repl:1:1
    at ContextifyScript.Script.runInThisContext (vm.js:44:33)
    at REPLServer.defaultEval (repl.js:239:29)

Support null-soaking on the draft state?

One of the major pain points when dealing with nested data in Javascript is not having a "null soak" (AKA existential) operator.

Coffeescript has a built-in one:

obj = { a: { b: 5 }}

# logs `5`
console.log obj.a?.b

# logs `undefined`
console.log obj.foo?.bar?.b

Immutable.js offers a similar pattern with getIn, albeit with a really-not-great API:

import { getIn } from 'immutable';

getIn({ x: { y: { z: 123 }}}, ['x', 'y', 'z']) // 123
getIn({ x: { y: { z: 123 }}}, ['x', 'q', 'p']) // undefined

I wonder if it would be possible/feasible to add such a solution for the draftState object in Immer? I imagine it'd be really nice to be able to do stuff like:

import produce from 'immer';

const initialState = { a: { b: 5 } }

const nextState = produce(initialState, (draftState) => {
  const {baz} = draftState.foo.bar;

  if (typeof baz === 'undefined') {
    someMutativeMethodToCreateBaz(draftState);
  }
})

With proxies, this is possible. A package called safe-proxy implements this. However I don't think an ES5 alternative would be doable (maybe with a try/catch or some really gross function.toString business?).

If this project is committed to maintaining an identical API with the ES5 version, it's probably more trouble than it's worth, unless I'm missing something?

Happy to contribute some work towards this idea because I think it could be a really neat addition, but yeah I also wouldn't be surprised if it's simply not worth doing :) mostly just wanted to start a discussion about it.

EDIT: actually, now that I think about it... having the API be totally implicit like that feels like a bad idea, since users who aren't familiar with Immer would believe that it's a native feature of the language. Could be really misleading.

We could export a helper that does it (like safe-proxy does), but really the user can just bring their own helper if they want to be able to do that.

Create es5 compatible version

If there is enough interest.

Proxy using defineProperty (the mobx way), use faux arrays and dirty check for key additions

lodash.find breaks proxy?

immer does not modify the draft value on using lodash.find on 1.0.0.

The test code is below:

        it("processes with lodash.find", () => {
            const base = [{id: 1, a: 1}, {id: 2, a: 1}]
            const result = produce(base, draft => {
                const obj1 = find(draft, {id: 1})
                const obj2 = find(draft, {id: 2})
                obj1.a = 2
                obj2.a = 2
            })
            expect(result[0].a).toEqual(2)
            expect(result[1].a).toEqual(2)
        })

result:

    Expected value to equal:
      2
    Received:
      1

      130 |                 obj2.a = 2
      131 |             })
    > 132 |             expect(result[0].a).toEqual(2)
      133 |             expect(result[1].a).toEqual(2)
      134 |         })
      135 |

for reproduce: https://github.com/iruca3/immer/tree/bug_with_lodash_find

It reproduces on 1.0.0, but does not reproduce on 0.8.5.
I tried to fix this, but I couldn't find out why this is caused.

create smaller standard built

Currently < 3KB, but, could be smaller by

  1. using rollup
  2. share common pieces of functionality between es5 and immer.js implementation
  3. make the classes methodless

[Question]Maybe the README example could be adjusted a little

// Plain reducer
function insertItem(array, action) {
    return [
        ...array.slice(0, action.index),
        action.item,
        ...array.slice(action.index)
    ]
}

--------------------------- I think the more real world way is: 
function insertItem(array, action) {
    return array.slice().splice(action.index, 0, action.item)
}

// With immer
function insertItem(array, action) {
    return produce(array, draft => {
        draft.splice(action.index, 0, action.item)
    })
}

// Plain reducer
function removeItem(array, action) {
    return [
        ...array.slice(0, action.index),
        ...array.slice(action.index + 1)
    ];
}

--------------------------- I think the more real world way is: 
function removeItem(array, action) {
    return array.slice().splice(action.index, 1)
}

// With immer
function removeItem(array, action) {
    return produce(array, draft => {
        draft.splice(action.index, 1)
    })
}

// Plain reducer
function updateObjectInArray(array, action) {
    return array.map( (item, index) => {
        if(index !== action.index) {
            // This isn't the item we care about - keep it as-is
            return item;
        }

        // Otherwise, this is the one we want - return an updated value
        return {
            ...item,
            ...action.item
        };
    });
}

--------------------------- I think the more real world way is just updateObject:
function updateObject(foosMap, action) {
   return {
      [action.item.id]: {
        ...foosMap[action.item.id],
        ...action.item
      }
   }

  // or more accurate (assume we just want to update bar)
  return lodash.merge({}, foosMap[action.item.id], {bar: action.bar})
}
because in most cases we will convert objectArray to object by using normalizr or something else

// With immer
function updateObjectInArray(array, action) {
    return produce(array, draft => {
        draft[action.index] = { ...item, ...action.item}
        // Alternatively, since arbitrarily deep updates are supported:
        // Object.assign(draft[action.index], action.item)
    })
}

Api name

I am not too excited about immer as function name as it is undescriptive. What should the idiomatic name be?

Transform? Update? withMutations? Make?

idea: swap arguments and support curry

(related to #24 )

idea:

  1. swap parameters (recipe, state) instead of other way around. It could be counterintuitive but it could help with partial application(or curry) and reusing transformations.
  2. if immer is called with only first argument (recipe function) it would not transform but return other function with recipe function bound
  3. allow for passing additional parameters to recipe function

WHY?
it would help reuse transformation, treat them as objects. For example when using with Redux #31 or with map support #35

const reducer = immer((state,  action) => {
    switch (action.type) {
        case 'add':
             state.arr.push(action.something);
            break;
    }
});

createStore(reducer, initialState);

Garbage collection of shadowed objects in immer arrays

I was investigating a solution for structural sharing of immutable JS arrays. The problem I had is that once I immutably mutate the array, it should give me a new array reference. If that old array reference still exists somewhere all the old data should still persist. However if that old array reference is dropped, then all the objects in that array that has been shadowed by the new array reference should also be garbage collected as they are no longer reachable. However I couldn't find a way to do this with just normal JS arrays or even pairing sparse arrays into a tree. What does this library do to deal with this problem?

I was thinking that JS needs some sort of weak pointers.

immerMap function

possibly add an immer implementation that can be used with map/filter/forEach

ex. 
const immerMap = (fn) => 
  (item) => immer( item, fn )

var arr = [['1'], ['2'], ['3']];
arr.map(immerMap((item) => {item.push('new')}));

Using 'produce' in selectors

I'm not sure if this is a issue with immer, which conceptually I love, or I'm using it wrong so I'd be interested to hear thoughts on this.

I'm trying to plug immer into a relatively small React/Redux project I have on the go. In reducers it all seems to work exactly as expected as the inputs are essentially a merge of the old state and an action payload.

However I then started looking at some of my selectors and thought it would be applicable there since the output from a selector is frequently a mutated version of a portion of the state, with lookups and/or references plugged in or one-time evaluations executed and saved. Again, that more or less seemed to work until I got to the part where a reference to a different part of the state tree was being attached to my new state. Then I got the following error,

Uncaught TypeError: Cannot assign to read only property '_id' of object '#<Object>'

In my case the objects are from a Mongo DB and so _id is the first field in the object.

To express this in more familiar terms, given an example of users and todos where each todo belongs to a user, my selector might be called getUserAndTodos and would return both the original user record and include a list of their todos taken from the general todo list in the global state, as well as maybe an 'allDone' flag to indicate whether that user has completed all their todos.

Given that I am essentially modifying (by addition) the user field from state, I thought this was a good place to use 'produce'. If 'produce' returns just the user record unmodified, then it's fine. If it additionally adds the 'allDone' field calculated from the users list of todos then it's also fine, but if I filter the todo list from state and add a list of the users todos to the user object I get the error above. Note, that if I turn off autofreeze then I don't see the error.

My guess is that because I am plugging in already frozen objects into a member of the 'new state' that somehow it's getting itself confused and trying to copy them over, rather than noticing it can just use a reference? More so because I am just referencing them and they aren't a direct argument to 'produce' itself. Or perhaps more like I've entirely misunderstood how this is supposed to work.

If necessary I can try and get a sandbox up that shows the error but I thought I would ask first in case I'm just doing it all wrong.

The partial stack trace of the error is below if it's of any use.

Thanks

TypeError: Cannot assign to read only property '_id' of object '#<Object>'
finalize
node_modules/immer/dist/immer.js:226
  223 |     var proto = Object.getPrototypeOf(base);
  224 |     if (proto === null || proto === Object.prototype) {
  225 |         for (var key in base) {
> 226 |             base[key] = finalize(base[key]);
  227 |         }return freeze(base);
  228 |     }
  229 | }
View compiled
finalize
node_modules/immer/dist/immer.js:226
  223 |     var proto = Object.getPrototypeOf(base);
  224 |     if (proto === null || proto === Object.prototype) {
  225 |         for (var key in base) {
> 226 |             base[key] = finalize(base[key]);
  227 |         }return freeze(base);
  228 |     }
  229 | }
View compiled
finalizeObject
node_modules/immer/dist/immer.js:237
  234 |     var copy = state.copy;
  235 |     var base = state.base;
  236 |     for (var prop in copy) {
> 237 |         if (copy[prop] !== base[prop]) copy[prop] = finalize(copy[prop]);
  238 |     }
  239 |     return freeze(copy);
  240 | }
View compiled
finalize
node_modules/immer/dist/immer.js:212
  209 |         if (state.finalized === true) return state.copy;
  210 |         state.finalized = true;
  211 |         if (Array.isArray(state.base)) return finalizeArray(state);
> 212 |         return finalizeObject(state);
  213 |     } else return state.base;
  214 | } else if (base !== null && (typeof base === "undefined" ? "undefined" : _typeof(base)) === "object") {
  215 |     // If finalize is called on an object that was not a proxy, it means that it is an object that was not there in the original
View compiled
produce
node_modules/immer/dist/immer.js:258
  255 | //values either than undefined will trigger warning;
  256 | !Object.is(maybeVoidReturn, undefined) && console.warn("Immer callback expects no return value. However " + (typeof maybeVoidReturn === "undefined" ? "undefined" : _typeof(maybeVoidReturn)) + " was returned");
  257 | // and finalize the modified proxy
> 258 | var res = finalize(rootClone);
  259 | // revoke all proxies
  260 | revocableProxies.forEach(function (p) {
  261 |     return p.revoke();
View compiled

ES5 is no longer ES5

After the 'immer -> produce' change, produce uses classes and arrow functions in the es5 build, causing IE11 to have a fit :)

flow error "unexpected token <" ?

Hi, I have flow 0.63.1 and while checking, I encountered this error

Error: node_modules/immer/dist/immer.js.flow:15
 15: declare export default function<S>(
                                    ^ Unexpected token <

Do you know what might be the cause? Thanks!

Support with Typescript?

Hi,

I am using typescript in my project. If Immer just uses native JS objects then it means that TS suggestions and compile errors should work right?

Cannot minify immer.js in create-react-app

The title says it all, basically. When I tried to create an optimized production build of a project that uses immer with create-react-app, I got told that immer.js cannot be minified. I believe this is because you do not terminate your statements with semicolons - the first error was on line 10. Is there anything that can be done about this?

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.