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 toSomerville
,Cambridge
changed toBoston
,- 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.
Using this setup, clicking a city will toggle the active
style on the city name:
So what happens if an active
city gets deleted?
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:
- We're calling the
deleteCity
method of the parent component (CityList
) - 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) - This change in state is causing
componentWillUpdate
to be called onCityList
, with that new state as one of the arguments - The update is causing
render
to be called again - The render method iterates over the
CityList
'scities
array, rendering a newCity
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:
- the second item in the list has a new name
- 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.
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.
- Index as a Key is an Anti-Pattern
- Why you need keys for collections in React
- Default Key for Dynamic Children
- React.js and Dynamic Children - Why the Keys are Important
- What is Virtual DOM
- Difference between virtual DOM and DOM
- React docs
Learn more about how The Gnar builds React applications.