19

I was trying to animate list insertion and removal with ReactCSSTransitionGroup, but the removal animation always animates only the last item of the list instead of the one that's being removed.

Here's a jsbin to illustrate this problem. Try pressing the "Add" button to verify that the insertion animation indeed works as expected, then click on the "x" besides any item to see the problem where the last item of the list is animated instead of the one you tried to remove.

Did I do something wrong while setting up the TransitionGroup or am I missing something in the CSS transition definitions?

Michelle Tilley
  • 157,729
  • 40
  • 374
  • 311
user14412
  • 987
  • 1
  • 11
  • 32

1 Answers1

29

You're experiencing this problem because you're using index as your key:

let nodes = items.map((item, index) => {
  let idx = index
  return (<Item key={index} value={item} index={index} _delete={this._onDelete}/>)
})

React uses the key property during virtual DOM diffing to figure out which element was removed, but indexes will never serve this purpose sufficiently.

Consider this example: you start off with the following array, which results in the following DOM structure:

const arr = [2, 4, 6, 8];

<li key={0}>2</li>
<li key={1}>4</li>
<li key={2}>6</li>
<li key={3}>8</li>

Then imagine you remove the element at index 2. You now have the following array, and the following DOM structure:

const arr = [2, 4, 8];

<li key={0}>2</li>
<li key={1}>4</li>
<li key={2}>8</li>

Notice that the 8 now resides in index 2; React sees that the difference between this DOM structure and the last one is that the li with key 3 is missing, so it removes it. So, no matter which array element you removed, the resulting DOM structure will be missing the li with key 3.

The solution is to use a unique identifier for each item in the list; in a real-life application, you might have an id field or some other primary key to use; for an app like this one, you can generate a incrementing ID:

let id = 0;
class List extends Component {
  constructor() {
    this.state = {
      items: [{id: ++id, value: 1}, {id: ++id, value: 2}]
    }

    // ...
  }

  _onClick(e) {
    this.state.items.push({id: ++id, value: Math.round(Math.random() * 10)})
    this.setState({items: this.state.items})
  }

  // ...

  render() {
    let items = this.state.items
    let nodes = items.map((item, index) => {
      let idx = index
      return (<Item key={item.id} value={item.value} index={index} _delete={this._onDelete}/>)
    })

    // ...
  }
}

Working example: http://jsbin.com/higofuhuni/2/edit

Michelle Tilley
  • 157,729
  • 40
  • 374
  • 311
  • Ah, you are right! Can't believe I missed that... I can't thank you enough for the effort you put into explaining the solution so clearly for me! I was building a component that displays a list of files uploaded, so even though they don't have an id on them, I can assign them temp ids that are unique (and not change when the list items change) just like you described. Thank you again! – user14412 May 24 '15 at 03:38
  • 3
    @Michelle Tilley: I am not able to see the output in the jsbin. Can you please help me to get the output since I m not clear on above concept in coding vice – albert Jegani Dec 28 '16 at 15:35
  • Soooo usefull that answer, and very well explained. Many thanks !! – Mehdi Bugnard Mar 01 '22 at 14:20
  • after the item at index 2 is removed, the item at index 3 will be moved to index 2, can the diffing algorithm not compare the element at index 2 of the previous virtual DOM with the index 2 of the current virtual DOM, spot the different and re-render the element? – Rongeegee Nov 14 '22 at 20:21
  • @Rongeegee Perhaps in the simplest of cases, but if you have a large array or stateful components this wouldn't work well. See also https://robinpokorny.com/blog/index-as-a-key-is-an-anti-pattern/ – Michelle Tilley Nov 15 '22 at 00:07