object-path-immutable icon indicating copy to clipboard operation
object-path-immutable copied to clipboard

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

Open lukechilds opened this issue 6 years ago • 7 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

lukechilds avatar Oct 26 '17 09:10 lukechilds

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

mariocasciaro avatar Oct 26 '17 15:10 mariocasciaro

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.

lukechilds avatar Oct 27 '17 01:10 lukechilds

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

lukechilds avatar Oct 27 '17 01:10 lukechilds

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.

mariocasciaro avatar Oct 27 '17 07:10 mariocasciaro

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.

lukechilds avatar Oct 27 '17 08:10 lukechilds

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

Prinzhorn avatar Jun 28 '18 13:06 Prinzhorn

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?

Prinzhorn avatar Jun 28 '18 13:06 Prinzhorn