React.js and Dynamic Children - Why the Keys are Important

… and check why 5600+ Rails engineers read also this

React.js and Dynamic Children - Why the Keys are Important

Recently I’ve been building a pretty dynamic interface based on google analytics data for one of our customers. There was a bug that I couldn’t quite figure out because of my wrong understanding of how react works. In the end it turned out to be a very easy fix and very important lesson on understanding react.js.

It is even pretty accurately described in react documentation however without showing what will go wrong and what kind of bugs you can expect if you don’t adhere to it. In other words, the documentation explains the right way but without going much into details about why.

Problem: Component doesn’t have initial state defined but the previous one

Let’s see the simplified version of my problem in action. I have two components. One is a list of countries and one is a list of cities in the country. When you click the country it becomes active and and the list of cities is refreshed as well to show only cities from this country. In real life there is much more data presented based on selected city and country but that’s enough for our demo.

The problem is however that when you select a second city and switch between the countries, the second city remain selected (on another list). We would like to always have first city on the list selected when country was switched. Here is a little demo:

Here is my code:

var CountriesComponent = React.createClass({
  getInitialState: function () {
    return { currentCountry: this.props.countries[0] };
  },
  handleClick: function(country, index, event){
    this.setState({currentCountry: country});
    event.preventDefault();
  },
  render: function(){
    return React.DOM.div({},
      TabList({
        elements: this.props.countries,
        clickHandler: this.handleClick
      }),
      TabList({
        elements: this.props.citiesPerCountry[this.state.currentCountry],
        clickHandler: function(city, index, event){ event.preventDefault(); }
      })
    );
  }
});

var TabList = React.createClass({
  getInitialState: function () {
    return { active: 0 };
  },
  render: function(){
    var tabs = [];
    for (var i = 0; i < this.props.elements.length; i++) {
      var className;
      if (i == this.state.active){
        className = "excited"
      } else {
        className = "neutral"
      }
      var tab = React.DOM.a({
        key: i,
        className: className,
        href: "#",
        onClick: (function(a, event){
          this.setState({active: a});
          this.props.clickHandler(this.props.elements[a], a, event);
        }).bind(this, i)
      }, this.props.elements[i]);
      tabs.push( tab );
    }
    return React.DOM.div({className:"list"}, tabs);
  }
});

Here is what I (unexperienced react padawan) was thinking:

Properties are immutable, state is mutable therefore if I change the properties of a component react will figure out it is a new component, create it and call getInitialState.

That was about this part of code:

  TabList({
    elements: this.props.citiesPerCountry[this.state.currentCountry],
    clickHandler: function(city, index, event){ event.preventDefault(); }
  })

I imagined that because I was changing properties (the elements key), then after changing the countries react was rendering a new cities list and a new city list should have first item selected because of my getInitialState:

var TabList = React.createClass({
  getInitialState: function () {
    return { active: 0 };
  }
});

But I was wrong. Wrong. Wrong…

React will determine whether it is the same component or not based on key

And when the key is not provided? Well… React will automatically use an increasing integer number; I suspect based on the data-reactid attribute in DOM.

So I was thinking that I am rendering conceptually a new component and react was actually rendering the old one. The fact that I changed props doesn’t matter. Immutability of props is just a convention, not a requirement. And react doesn’t care. Because it was the same component getInitialState was not called and the component remembered its old state.

New component on the same level as old one? Would you kindly change a key?

The fix is easy. Change the component key and react will know that it is a different component and not the same one. You can try it below. When you select a new country, the city is always the first one.

And the code:

var CountriesComponent = React.createClass({
  getInitialState: function () {
    return { currentCountry: this.props.countries[0] };
  },
  handleClick: function(country, index, event){
    this.setState({currentCountry: country});
    event.preventDefault();
  },
  render: function(){
    return React.DOM.div({},
      TabList({
        key: "countriesList",
        elements: this.props.countries,
        clickHandler: this.handleClick
      }),
      TabList({
        key: this.state.currentCountry,
        elements: this.props.citiesPerCountry[this.state.currentCountry],
        clickHandler: function(city, index, event){ event.preventDefault(); }
      })
    );
  }
});

The important part is the key for second list:

TabList({
  key: this.state.currentCountry,
  elements: this.props.citiesPerCountry[this.state.currentCountry],
  clickHandler: function(city, index, event){ event.preventDefault(); }
})

You can see in DOM a different data-reactid attribute value now based on the provided key.

What did I just learn?

Now the I’ve actually been hit by this and I understood the problem few more things makes sense…

  • React documentation on dynamic children

    The situation gets more complicated when the children are shuffled around (as in search results) or if new components are added onto the front of the list (as in streams). In these cases where the identity and state of each child must be maintained across render passes, you can uniquely identify each child by assigning it a key

  • Props in getInitialState Is an Anti-Pattern

    OK, guilty as charged… Using props, passed down from parent, to generate state in getInitialState often leads to duplication of “source of truth”, i.e. where the real data is. Whenever possible, compute values on-the-fly to ensure that they don’t get out of sync later on and cause maintenance trouble.

    In this example I remember in top-level CountriesComponent currently selected country because it is used to provide list of cities for the second list. But I also keep this as active state in the first list.

  • You wouldn’t have this problem if you didn’t keep state in this component

    This is what my coworkers said to me when I described my problem to them.

    It appears that it is often easier and wiser to move the state higher in component hierarchy. Maybe the list itself does not need to keep the state itself. Why? Because the parent component is interested that the list element was clicked. Something new was selected and not only the list must change and rerender but also another parts of parent component must change. If parent component knows about list status, then it can render the list with new status, new currently select item.

    Notice that in my example I only need to change the key of cities list when country is changed because I keep state in that list. If I didn’t I wouldn’t have to and I wouldn’t care.

Now go home and…

  • give keys to your children
  • keep the state in one place
  • remember: dynamic stateless components are easier and can live fine without key (althought it is best practice to provide them and react will warn you when you don’t); for stateful components proper key is a must have!

One more thing

If you liked this blogpost you might enjoy our books about React.js .

You might also like