-
Notifications
You must be signed in to change notification settings - Fork 51
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
Reordering items in list cursor will confuse updates on captured items. #89
Comments
I believe reason for this is that |
In my understanding this could also cause issue when |
I think these are the lines causing the issue: Here is a little demonstration of what I think is going on: let root = Cursor.from(Immutable.fromJS({items: [{x: 3}, {x: 2}, {x: 1}]}))
let x1 = root.cursor('items').cursor(0);
let root2 = root.updateIn(['items'], items => items.reverse());
let x2 = root2.cursor('items').cursor(2);
x1.deref() === x2.deref() // => true
x1._keyPath === x2.__keyPath // => false So what I believe is happening is Component rendering x1 first and then rendering x2 afterwards won't re-render since I was really hoping that |
I made a demo: http://jsbin.com/ramibe/3/edit?js,output This are the right invariants to make/assume: x1.deref() === x2.deref() // => true
x1._keyPath === x2.__keyPath // => false I added a "side effect" to fetch keys externally: http://jsbin.com/ramibe/4/edit?js,output |
Here I made an example to reproduce this issue: Click "x" few times then "reverse" then "x" again, see what happens! |
Okay. I definitely see it now. Here's a fix: http://jsbin.com/bomoni/5/edit (translated to JSX for readability) <div>
{items.toArray().map((item, idx) => {
return <Item.jsx key={item.get('id')} _idx={idx} item={item} />
})}
</div> The problem is that we should somehow mark a component as "dirty" for Note that attempting to provide |
Actually. You just need to add the order property to the key. This works without manually passing an ambiguous prop: <div>
{items.toArray().map((item, idx) => <Item.jsx key={item.get('id') + '.' + idx} item={item} />)}
</div> This is a non-issue with omniscient and/or immstruct. |
I think this is what was happening: If the key was just This is noticeable that if you turn on debug: |
I don't agree. As pointed out the reason react decided to not re-render is because Also note that this is not an issue with plain react as there |
As of encoding |
One way to solve that would be to update implementation of function isEqualCursor (a, b) {
return _unCursor(a) === _unCursor(b) &&
a.__keyPath === b.__keyPath;
} Although that is still not an ideal solution (yet better than no solution) as component's render will be re-triggered in order for it to return equal DOM tree. As I already mentioned in a few different threads, react itself updates Doing same in omniscient is little more tricky as
Probably it would make sense to just do a simple fix to a |
Alright, I'm inclined to see that this may be feasible. Here's a demo using your proposal: http://jsbin.com/veveme/7/edit?js,output Using: function isEqualCursor (a, b) {
var
n = a._keyPath.length - 1,
m = n >= 0 ? n : 0;
return _unCursor(a) === _unCursor(b) &&
n === b._keyPath.length &&
a._keyPath[m] === b._keyPath[m];
} I didn't do full array comparison. For stronger comparison, I'll consider hashing the keypaths every time a new cursor is produced at a new keypath for custom cursor types. |
I have added another version of the example illustrating that adding index to a key is not a general solution that going to work in all cases: |
Conceptual fix for #89 (comment) by manually passing |
Do we want to go down the path of relying on cursor implementation details to make this work? Is the issue really that we are closing over props when creating event handlers inside render? Edit: @Gozala how did your immutablejs issue of adding |
I definitely see that this would be an issue, but fixing the keypath feels like fixing the symptom, not the problem. Also, it probably doesnt belong in immutable.js equals, as it's comparing values, and two cursors' value ought to be equal even if they belong in separate structures. Don't really know how we'd go about fixing it though.. |
..if we still suggest closing over cursors in render for event handlers. Moving handlers to members on the component would circumvent the issues, though I agree it's not as nice as just inlining them in render const Item = component({
onClick: function () {
this.props.item.update(i => {
return i.set('value', i.get('value') + 1)
});
}
},
function Item ({item}) {
return <button key={item.get('name')} onClick={this.onClick}>
{item.get('name')} - {item.get('value')}
</button>;
});
const Items = component({
onClick: function () {
this.props.items.update(is => is.reverse())
}
},
function Items ({items}) {
const reverse = <button onClick={this.onClick}>Reverse</button>;
return <div>
{reverse}
{items.toArray().map(item =>
Item({ key: item.get('name'), item }))}
</div>;
}); Edit: runnable example http://goo.gl/RckY2W |
I'm guessing this also ought to work const Item = component(function Item ({item}) {
const onClick = () => this.props.item.update(i => i.set('value', i.get('value') + 1));
return <button key={item.get('name')} onClick={onClick}>
{item.get('name')} - {item.get('value')}
</button>;
}); Edit: Come to think of it, is this because props is a new object on each render? Otherwise you'd think at least this would work const Item = component(function Item (props) {
const onClick = () => props.item.update(i => i.set('value', i.get('value') + 1)); // no this
... |
Yes that will work, although I really would like to avoid depending on |
This probably going to work but you will end up re-runnig |
It might be possible to abstract away depending on |
I think proper solution for this problem would be to do whatever we end up implementing for event handler. Meaning that we do swap event handlers even if render has not run, I think something along these lines could be done for cursors as well. |
That's what I was thinking. Having a decoration for event handlers. We've been discussing this some back and forth, without having concluded with a one-fit-all solution, but we need a concrete, goto, way of doing event handlers. One solution might be decorators (just thinking of the top of my head now) MyComponent({
cursor: myCursor,
onClick: eventHandler(({cursor}) => cursor.set('foo', 'bar'))
}); Where |
Disclamer: I think it is very likely that project I'm working on is going to drop cursors in favour of solution inspired by elm which is illustrated in this jsbin: There has being several reasons, I'm going to quote some of the relevant discussion below:
|
Me too. |
Saw the elm-like approach from your immutable.js issue post, I was intrigued! I've always thought of the cursors of immutable.js as kind of noisy |
@torgeir I'm also considering use of CSP channels as an alternative to the illustrated solution, although I have not took time to evaluate pros and cons yet. Likely we'll start with what I've illustrated and maybe consider channels in parallel. |
The Edit: changed the link, messed up the keys in the first example |
I know this thread is a couple months old, but I am fascinated by this approach. How has it been working out @Gozala ? |
I believe following scenario will cause an issues that we have being observing in our application code:
Let's say this is more or less our root cursor model
{items: [a, b, c]}
:c
item.c
item down to a subcomponent.items
to be reversed (without causing component thatc
was passed to to be re-triggered).c
cursor makes an update onc
.Outcome of this steps is that
a
will be replaced withc
causing us to have end up with{items: [c, b, c]}
.The text was updated successfully, but these errors were encountered: