Articles

Why React needs a key prop

Kent C. Dodds
Kent C. Dodds

If you've used React for a while, you've probably run into this before:

index.js:1 Warning: Each child in a list should have a unique "key" prop.
Check the render method of `App`. See https://reactjs.org/link/warning-keys for more information.
at li
at App (http://localhost:3000/static/js/30.chunk.js:48:75)

You probably know the solution as well. Here's the rule:

Whenever you're rendering an array of React elements, each one must have a unique key prop.

Here's the simplest way to reproduce this problem:

const items = [
{id: 'apple', value: '๐ŸŽ apple'},
{id: 'orange', value: '๐ŸŠ orange'},
{id: 'grape', value: '๐Ÿ‡ grape'},
{id: 'pear', value: '๐Ÿ pear'},
]
function App() {
return (
<ul>
{items.map((item) => (
<li>{item.value}</li>
))}
</ul>
)
}

And the solution is pretty simple:

- <li>{item.value}</li>
+ <li key={item.id}>{item.value}</li>

But like... Why? React doesn't seem to have a problem if I were to write it like this:

function App() {
return (
<ul>
<li>๐ŸŽ apple</li>
<li>๐ŸŠ orange</li>
<li>๐Ÿ‡ grape</li>
<li>๐Ÿ pear</li>
</ul>
)
}

It's only a problem if I try to pass the elements as an array. So yeah, why does React have such a big problem with arrays anyway?

First, if you want to understand what problems can happen without the key prop (and cool things you can do with it), read Understanding React's key prop. In this post, I just want to explain why React can't just magically make things work without a key prop.

I want you to pretend you're React and I'm a React developer building an application. I'm writing a function component called App. You don't get to see my implementation, all you get is the function which you can pass arguments to and receive a return value from.

So let's say I give you that function and tell you to render that to the page. So you call App and you get the following React elements back (learn more about React elements):

const element = {
type: 'ul',
key: null,
props: {
children: [
{type: 'li', key: null, props: {children: '๐ŸŽ apple'}},
{type: 'li', key: null, props: {children: '๐ŸŠ orange'}},
{type: 'li', key: null, props: {children: '๐Ÿ‡ grape'}},
{type: 'li', key: null, props: {children: '๐Ÿ pear'}},
],
},
}

Ok, first, you're going to warn right? Because the key is null for all of those children. ๐Ÿ˜‰ But really, it's fine. You're React and you know how to take these elements and render them to the page. So you render the ul and lis to the page. All's good.

Ok, so now my App calls a state updater function and so you know you need to re-render. So you call App again and this time you get some new React elements:

const element = {
type: 'ul',
key: null,
props: {
children: [
{type: 'li', key: null, props: {children: '๐ŸŽ apple'}},
{type: 'li', key: null, props: {children: '๐ŸŠ orange'}},
{type: 'li', key: null, props: {children: '๐Ÿ pear'}},
],
},
}

Ok, so you do a quick and easy diff here:

- {type: 'li', key: null, props: {children: '๐Ÿ‡ grape'}},

Oh, cool, so you can just remove the li from the page right? Yeah, go ahead and just unmount it. Sure, do that. Easy-peasy.

Ooh! I need another state update, so you get the next set of React elements:

const element = {
type: 'ul',
key: null,
props: {
children: [
{type: 'li', key: null, props: {children: '๐ŸŠ apple'}},
{type: 'li', key: null, props: {children: '๐Ÿ pear'}},
],
},
}

Now do another diff:

- {type: 'li', key: null, props: {children: '๐ŸŽ apple'}},
- {type: 'li', key: null, props: {children: '๐ŸŠ orange'}},
+ {type: 'li', key: null, props: {children: '๐ŸŠ apple'}},

Hmm... Ok, so now... Should remove the ๐ŸŽ apple and ๐ŸŠ orange lis and create a new one with ๐ŸŠ apple? How certain are you that this is what I meant to do? Maybe what I actually want is to remove the ๐ŸŽ apple and rename ๐ŸŠ orange to ๐ŸŠ apple... Or maybe I want to remove the ๐ŸŠ orange and rename ๐ŸŽ apple to ๐ŸŠ apple. Or maybe I want to remove ๐Ÿ pear, then rename ๐ŸŠ orange to ๐Ÿ pear, and then rename ๐ŸŽ apple to ๐ŸŠ apple...

Now you're thinking "Ugh. What the heck? Mr. Dodds. Give me a break."

So, as React, you do your best guess: You pretend the key is the index. I mean, you gotta do something right? This is what React actually does.

Interestingly, this means that rather than just unmounting the ๐Ÿ‡ grape li on that first state update, you actually unmounted ๐Ÿ pear and updated ๐Ÿ‡ grape to say ๐Ÿ pear ๐Ÿ˜‚. But in a real app, this is no laughing matter. Unmounting and mounting components has implications on the state and side-effects of a component (which I describe in more detail in the aforementioned blog post).

And that also means with this last state update, you unmounted ๐Ÿ pear again and updated ๐ŸŽ apple to be ๐ŸŠ apple and ๐ŸŠ orange got updated to say ๐Ÿ pear. What a disaster!

This is definitely not the most optimal, but what are you gonna do? You have no idea what I meant with that element change!

(By the way, I used this code to validate what was going on, if you want to poke around).

In walks the key prop...

Phew... Wouldn't it be nice if I had given you some way to track the React elements from one render to the next so your diff could be more informative. That way you'd know my intent much better right?

Yeah, let's try that last one, only with keys this time:

const element = {
type: 'ul',
key: null,
props: {
children: [
{type: 'li', key: 'apple', props: {children: '๐ŸŽ apple'}},
{type: 'li', key: 'orange', props: {children: '๐ŸŠ orange'}},
{type: 'li', key: 'pear', props: {children: '๐Ÿ pear'}},
],
},
}

Ok, nice, so now here comes the update for you:

const element = {
type: 'ul',
key: null,
props: {
children: [
{type: 'li', key: 'apple', props: {children: '๐ŸŠ apple'}},
{type: 'li', key: 'pear', props: {children: '๐Ÿ pear'}},
],
},
}

Ha! Now you know exactly what to do! You can now do a much more granular diff. First you notice that the element with the key of 'orange' is gone, so you know you can remove that from the page. And the element with the key of 'apple' has new children so you can simply update it and move on with life without worrying about whether you're doing the right thing.

Oh, and for our very first change when we removed the grape li, you can correctly just remove that one now too.

Conclusion

Now that you've put yourself in React's shoes, hopefully you understand why React can't just magically know what you're doing with your elements and why you need to provide a key prop. Frankly, this is a limitation in React. It would be nice to not have this limitation, but not at the expense of React's simplicity (which is what I love most about React). I hope this article helped you get an solid understanding of why the key prop is needed when rendering arrays.

I'll leave you with one more thought... With your understanding of the key prop now, can you think of situations where you could use a key prop outside an array? The key prop has less to do with arrays, and more to do with controlling when a component is reused or disposed of and created anew. Give that some thought ๐Ÿ˜„

Get my free 7-part email course on React!

Delivered straight to your inbox.