Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is React's DOM reconciliation not working as expected?

Tags:

reactjs

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 divs 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

like image 236
Pavel Pomerantsev Avatar asked Oct 29 '22 23:10

Pavel Pomerantsev


1 Answers

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>;
  }
});
like image 52
azium Avatar answered Dec 22 '22 09:12

azium