mariocasciaro / object-path-immutable

Modify deep object properties without modifying the original object (immutability). Works great with React and Redux.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Should not create a new object if the values are the same

lukechilds opened this issue · comments

I'm not sure if this is a bug or if I'm misunderstanding how this is supposed to work.

I assumed in the following example one and two would be the same object.

const obj = { foo: 0 };
const one = immutable(obj).set('foo', 'bar').value();
// { foo: 'bar' };
const two = immutable(one).set('foo', 'bar').value();
// { foo: 'bar' };
one === two
// false

That's exactly the intended behaviour. If you don't want a new object to be created, just chain the set() methods and call value() at the end.

immutable(obj).set('foo', 'bar').set('foo2', 'bar').value();

Maybe I didn't explain very well, that was an oversimplified example.

The above code is happening inside an animation loop. A more accurate example would be:

getAnimationState(oldAnimationState) {
  const newAnimationState = immutable(oldAnimationState);
  this.tweens.forEach(tween => newAnimationState.set(tween.path, tween.value));
  return newAnimationState.value();
}

getAnimationState() runs at around 60 times per second, however with the above code if the tween values are identical between frames, I get a new object returned due to calling .set() and so my app re-renders every frame.

Can easily fix with:

getAnimationState(oldAnimationState) {
  const newAnimationState = immutable(oldAnimationState);
  this.tweens.forEach(tween => {
    const oldValue = objectPath.get(oldAnimationState, tween.path);
    if(oldValue !== tween.value) {
      newAnimationState.set(tween.path, tween.value);
    }
  });
  return newAnimationState.value();
}

But I was expecting this to be handled internally. Only realised that wasn't the case when I was doing performance profiling.

Seems like if both objects deep equal each other then they should be the same reference.

To summarise, if all the tween values are the same between frames, I was expecting newAnimationState === oldAnimationState to be true. object-path-immutable would just need a oldVal === newVal check inside .set() and noop if it's true. Seems like this would be a pretty simple change for a fairly common use case, and the current behaviour could be causing large performance penalties for developers who assume this is handled internally.

If it makes any difference to you, I just tested and immutability-helper behaves the way I'm proposing.

import update from 'immutability-helper';

const obj = { foo: 0 };
const one = update(obj, { foo: { $set: 1 } });
const two = update(one, { foo: { $set: 1 } });
one === two
// true

Thanks for the more detailed description. I get it now and it makes sense. We'll track this as an enhancement as it's going to change the current behaviour of a few methods.
Thanks for reporting Luke.

No probs, I'm pretty busy atm with paid work but happy to submit a PR for this when I've got some free time.

FWIW Immutable.js behaves the same

const { Map } = require('immutable');
const map = Map({ a: 1 });

console.log(map === map.set('a', 1));
//true
console.log(map.set('a', 1) === map.set('a', 1));
//true

I agree that the behavior should be the same and that every operation that can be a noop should be a noop.

My non-pseudo code use-case looks like this and I'd love to not have to check each property individually (which are controlled input elements) but compare the immutable instances

let changedStory = immutable(this.props.story)
  .set('title', this.state.title)
  .set('description', this.state.description)
  .set('language', this.state.language)
  .value();

if(changedStory !== this.props.story) {
  this.props.editor.onStoryChange(changedStory);
}

Was looking through the code and came to the conclusion that it is non-trivial at least for someone not comfortable with the code base. The problem is that the code works top-down and clones all objects along the path, but only at the very end inside changeCallback do we know if that was actually needed. At this point we could return the original thing, but still wasted time on all the cloning.

So how do we peek at the bottom to see if it would be a noop without duplicating most of the walking-logic? Or how do we delay/schedule the cloning until that point?