I'm trying to swap two children of an element with a transition using React.
<div style={{position: 'relative'}}>
{this.state.items.map((item, index) => (
<div
key={item}
style={{position: 'absolute',
transform: `translateY(${index * 20}px)`,
transition: '1s linear transform'}}>
{item}
</div>
))}
</div>
state.items
is an array of two items. When it is reordered, the two child div
s should transition to their new positions.
What happens in reality is while the second element transitions as expected, the first one jumps instantly.
As far as I can tell, React thinks that it can reuse one of the child elements, but not the other, although the docs say that if we use the key
attribute, it should always reuse elements: https://facebook.github.io/react/docs/reconciliation.html (at least, that's how I understand it).
What should I change in my code to make it work as expected? Or is it a bug in React?
Live example: http://codepen.io/pavelp/pen/jAkoAG
caveat: I'm making some assumptions in this answer, nevertheless it shines light some of your (and previously my) questions. Also my solution can almost certainly be simplified, but for the purposes of answering this question it should be adequate.
This is a great question. I was a bit surprised to open up the dev tools and see what's actually happening when swapping the items.
If you take a look, you can sort of see what React is up to. The second element is not changing its style prop at all and just swaps the inner text node, while the first element is dropped into the dom as a fresh element.
If I had to guess, this is because of the way swapping two items in a array works, where at least one item is copied to a temp variable and placed back into the array.
I thought that maybe if you make the translation random, both elements would get new style props and animate, but that only made it more clear this was not the intended behaviour.
On the way to finding a solution:
As an experiment, what if we created the nodes ahead of time, and pass the index prop in render via React.cloneElement
. While we're at it, let's render a span
if index === 0
and a div
otherwise. No keys to worry about.
http://codepen.io/alex-wilmer/pen/pbaXzQ?editors=1010
Opening up the dev tools now illustrates exactly what React is intending. React is preserving the elements and only changing the relevant part, in this case the innerText node and the element type. Because the styles are swapped exactly 1 : 1, no style update is needed.
Solution:
You can generate your React elements ahead of time, keep those in an array, and as such there are no keys to shuffle around and figure out how to place back into the DOM. Then use a different array to keep track of the intended order. Possibly highly convoluted, but it works!
http://codepen.io/alex-wilmer/pen/kXZKoN?editors=1010
const Item = function (props) {
return (
<div
style={{position: 'absolute',
transform: `translateY(${props.index * 20}px)`,
transition: '0.5s linear transform'}}>
{props.children}
</div>
)
}
const App = React.createClass({
getInitialState () {
return {
items: [
{item: 'One', C: <Item>One</Item>},
{item: 'Two', C: <Item>Two</Item>}
],
order: ['One', 'Two']
};
},
swap () {
this.setState({
order: [this.state.order[1], this.state.order[0]]
});
},
render: function () {
return <div>
<button onClick={this.swap}>Swap</button>
<div style={{position: 'relative'}}>
{this.state.items.map(x =>
React.cloneElement(x.C, {
index: this.state.order.findIndex(z => z === x.item)
}))
}
</div>
</div>;
}
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With