7/02/2016

[React] Don't Use Loop Index as Component Key

var ResultList = React.createClass({
  getInitialState: function() {
    return {results:[{id:1,text:'hello'}]};
  }, 
  onclick: function() {
    this.setState( {results:[{id:1},{id:2}]} );
  }, 
  render: function() {
    return (
      <div> 
   <button onClick={this.onclick} >click me!</button>
      <ul>
        {this.state.results.map(function(result, i) {
           return <ResultItem result={result}/>;
        })}
      </ul>
      </div>
    );
  }
});

var ResultItem = React.createClass({
  getInitialState: function() {
    return {clicked:false};
  },
  onclick: function() {
    this.setState( {clicked: !this.state.clicked } );
  }, 
  render: function() {
    return (
      <li onClick={this.onclick}>{' [clicked state] =  ' + this.state.clicked}</li>
    );
  }
});

ReactDOM.render(<ResultList />, mountNode);
If you render a list of components in a loop, don't forget to add key attribute to each of the component or React will give you a warning message like: "Each child in an array should have a unique "key" prop. Check the render method of undefined. See http://fb.me/react-warning-keys for more information".
To solve this problem, many people may just make a loop index as component key as below. Well, this solution does make the warning message disappear. However, it's an anti-pattern and will make your stateful components have incorrect state.
{this.state.results.map(function(result, i) {
  return <ResultItem key={i} result={result}/>;
})}
React's diff algorithm will try to find the min steps to add, remove, and update component's state in O(n) time. When it detects the component's class name (i.e., ResultItem) is the same and have the same key, it assumes the component instance is the same one. Therefore, React will not update this component's state.
So when the button is clicked in ResultList, you may think the ResultItem's result props is changed, then ResultItem will be re-inializated with the initial state {clicked: false}. It's not true. Since React think the ResultItem with id = 1 is the old instance, the state still remains the old one.
The correct to add a key to component is just use the unique id in the database like:
{this.state.results.map(function(result, i) {
   return <ResultItem key={result.id} result={result}/>;
})}
It's noted that the key should be added to a component's props instead of added to the children's HTML. Otherwise, React's diff algorithm will not know the children component's id.
// WRONG!
var ResultItem = React.createClass({
  render: function() {
    return (
      <li key={this.props.result.id}></li>
    );
  }
});

Ref: https://facebook.github.io/react/docs/multiple-components.html#dynamic-children