The Gnar Company
The Gnar Company

Using the index as the iterator key in React apps is an antipattern

One nice benefit of using React is having the ability to break down your UI into reusable and composable components. This is useful when trying to iterate over a list of items, like a collection of cities:

render() {
  return (
    <div>
      <City name='Boston' />
      <City name='Cambridge' />
      <City name='Somerville' />
    </div>
  )
}

This is repetitive and we can do a lot better by mapping over an array that contains those elements, returning a new City for each one:

render() {
  const cities = [
    { name: 'Boston' },
    { name: 'Cambridge' },
    { name: 'Somerville' },
  ]

  return (
    <div>
      {cities.map(city => <City name={city.name} />)}
    </div>
  )
}

However, as you've likely run into, doing this - as is - will cause React to log a warning in the JavaScript console for not providing a key prop. This probably looks familiar:

Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of `CityList`. See https://fb.me/react-warning-keys for more information.

In order to get rid of this error, we need to provide a key prop to the child (in this case, City). Since the key prop has to be unique for each component, it is common to use the array's index when mapping as the value for the component's key prop.

render() {
  const cities = [
    { name: 'Boston' },
    { name: 'Cambridge' },
    { name: 'Somerville' },
  ]

  return (
    <div>
    {
      cities.map((city, index) => (
        <City
          key={index}
          name={city.name}
        />
      ))
    }
    </div>
  )
}

After all, why not? It's a unique identifer that's automatically generated and incremented for each element of the array we're mapping over. Plenty of guides offer it as the solution to that error message, and if we don't provide anything, React will use the index anyway.

So what's wrong with it?

In most cases, nothing is wrong with this; React just needs a way to track elements, so if you're returning a static list of repeated content that will never change or get reordered or filtered, the index is fine. However, data in lists often is added, removed, filtered, or reordered.

Part of React's magic is absolving you of the responsibility for updating the DOM. This means you don't have to track down the element you want to change in the DOM and update it manually. If an item in a list needed to change, sometimes that entire list would have to be rebuilt to reflect the changed item. This isn't ideal - the trouble is that the DOM was never really optimized for creating the types of dynamic UI users have come to expect in webpages now, so although changing the DOM isn't really that time-intensive, re-rendering it is. Whenever it gets changed the CSS has to be recalculated and all of the elements within it have to be repainted, and when there's enough going on that can mean a big hit to your application's performance.

React handles this by creating a virtual DOM that represents the DOM. When a change happens, an entirely new virtual DOM is created, and the new one is compared to the old one to determine the difference, allowing React to update only the pieces that need to be updated. Since React is declarative (not imperative), all you need to do is let it know what the DOM needs to look like, and it takes care of making that happen. It's no longer necessary to scan through child nodes of random elements to identify and change specific ones.

The key prop helps React make this comparison to determine which DOM elements have changed, were added, or were removed. So to go back to the original question, what's wrong with using the index? Two things: performance and identity.

Performance

Suppose you have a simple list of elements that use the index as the key, and we add an item to the beginning:

<ul>                          |     <ul>
  <li key={0}>Boston</li>     |       <li key={0}>Somerville</li>
  <li key={1}>Cambridge</li>  |       <li key={1}>Boston</li>
</ul>                         |       <li key={2}>Cambridge</li>
                              |     </ul>

Although to us it's clear that Boston and Cambridge did not change and the only difference is the addition of Somerville to the top of the list, to React this looks like:

  • Boston changed to Somerville,
  • Cambridge changed to Boston,
  • A new item named Cambridge got added to the end of the list.

A simple addition in the wrong place caused the entire list to get redrawn.

If we instead use a truly unique (and unchanging) identifier for the key, this change becomes obvious to React as well:

<ul>                                    |     <ul>
  <li key="boston">Boston</li>          |       <li key="somerville">Somerville</li>
  <li key="cambridge">Cambridge</li>    |       <li key="boston">Boston</li>
</ul>                                   |       <li key="cambridge">Cambridge</li>
                                        |     </ul>

This assumes there won't be any duplicated city names. Another (preferable) alternative would be to use an object id, assuming this data came from a database somewhere instead of being a hardcoded list of cities.

<ul>                          |     <ul>
  <li key={1}>Boston</li>     |       <li key={3}>Somerville</li>
  <li key={2}>Cambridge</li>  |       <li key={1}>Boston</li>
</ul>                         |       <li key={2}>Cambridge</li>
                              |     </ul>

Identity

Using a unique key prop provides the above performance benefits because at its heart the purpose of the key is to provide elements with identity. This becomes clear when changing the elements in a list. Suppose you have a list of cities that can take different styles based on some logic contained in their component state.

City.jsx

CityList.jsx

Using this setup, clicking a city will toggle the active style on the city name:

activate

So what happens if an active city gets deleted?

delete

All of a sudden Somerville became active without being clicked on. Let's dive into the details to figure out why.

Although it feels like we're physically deleting something from the DOM, in reality what's happening is:

  1. We're calling the deleteCity method of the parent component (CityList)

  2. That method is calling this.setState to remove the chosen city (Cambridge) from the parent component's state (well, more accurately, it's returning a new array that doesn't include the chosen city)

  3. This change in state is causing componentWillUpdate to be called on CityList, with that new state as one of the arguments

  4. The update is causing render to be called again

  5. The render method iterates over the CityList's cities array, rendering a new City component for each element in the array

However, this is where there's a catch. In order to maximize speed, none of this has yet caused the DOM to update; these changes have instead been created within a new virtual DOM. React will now compare this virtual DOM against the version of the virtual DOM that was present immediately before we clicked on the X to delete Cambridge.

In this new version of the virtual DOM, everything is the same except at index/key = 1, the name prop has changed from 'Cambridge' to 'Somerville'. There was never a setState call on the component with that key, so as far as React's virtual DOM diffing algorithim is concerned, the only things that changed were that:

  1. the second item in the list has a new name
  2. the third item in the list got deleted

As a result, what looks like Somerville's state spontaenously changing to match Cambridge's state is actually just the second city's name prop changing from 'Cambridge' to 'Somerville', and the last city getting deleted.

Solution

Simply changing the key prop from index to a unique identifier like an object id fixes this for us.

CityList.jsx

fixed

By simply adding a unique index to our data, we were able to cicumvent the problem entirely.

Conclusion

This is a fairly contrived example, but the same problem has bitten me in two different real apps. Regardless, understanding the reason why a key prop is necessary in the first place is really helpful for simply understanding what React is doing under the hood. I also think people should stop teaching that the index is an acceptable key without at least making the point that it should only be used as a last resort.

This obviously isn't the first blog post written about this topic - there are a bunch of others that go into varying degrees of depth about it, so check them out too if you're interested in learning more.