Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
lukechilds opened this issue Oct 26, 2017 · 7 comments
Open

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

lukechilds opened this issue Oct 26, 2017 · 7 comments

Comments

@lukechilds
Copy link

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
@mariocasciaro
Copy link
Owner

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

@lukechilds
Copy link
Author

lukechilds commented Oct 27, 2017

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
Copy link
Author

lukechilds commented Oct 27, 2017

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

@mariocasciaro
Copy link
Owner

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.

@lukechilds
Copy link
Author

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.

@Prinzhorn
Copy link
Contributor

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
Copy link
Contributor

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants