If you ever have rendered a list of items using a dynamic looping method in React you probably encountered either ESLint or a console error being thrown at you.
Is this error familiar to you? Let's find out why it's thrown and why React want us to add a "key" prop to our dynamic list items.
Why?
React uses the key
prop to uniquely identify a component. Then it uses this information to decide what to do when the source list changes and a re-render is triggered.
As described in the React docs:
Keys tell React which array item each component corresponds to, so that it can match them up later. This becomes important if your array items can move (e.g. due to sorting), get inserted, or get deleted. A well-chosen key helps React infer what exactly has happened, and make the correct updates to the DOM tree.
Every time React encounters an unknown key it will (re-)create a component and the DOM.
I created a simple component that shows a list of countries with the options to add or remove a country.
const defaultCountries = [
{ name: "Argentina", capital: "Buenos Aires" },
{ name: "Belgium", capital: "Brussels" },
{ name: "The Netherlands", capital: "Amsterdam" },
{ name: "Brazil", capital: "Brasília" },
{ name: "Vietnam", capital: "Hanoi" },
];
function Countries() {
const [countries, setCountries] =
useState<{ name: string; capital: string }[]>(defaultCountries);
return (
<div className="center">
<h1>Countries</h1>
<ul>
{countries.map((country) => (
<Item
name={country.name}
capital={country.capital}
onDelete={(name) => {
setCountries((currentState) =>
currentState.filter(
(currentCountry) => currentCountry.name !== name
)
);
}}
/>
))}
</ul>
<form
ref={formRef}
onSubmit={(event) => {
event.preventDefault();
if (!formRef.current) {
return;
}
const data = new FormData(formRef.current);
const name = data.get("country");
const capital = data.get("capital");
if (typeof name !== "string" || typeof capital !== "string") {
return;
}
setCountries((prev) => [...prev, { name, capital }]);
formRef.current.reset();
focusRef.current?.focus();
}}
>
<input
ref={focusRef}
id="country"
name="country"
type="text"
placeholder="Country"
/>
<input id="capital" name="capital" type="text" placeholder="Capital" />
<button className="button">Add</button>
</form>
</div>
);
}
interface ItemProps {
name: string;
capital: string;
onDelete: (name: string) => void;
}
export function Item({ name, capital, onDelete }: ItemProps) {
return (
<li className="item">
<strong>{name}: </strong>
{capital}
<button
className="button"
type="button"
onClick={() => {
onDelete(name);
}}
>
Remove
</button>
</li>
);
}
Let's say we pass a random key value to the Item
component on every render:
<Item
key={Math.random()}
// ...
/>
Let's look what happens in the Chrome inspector when we add or remove countries from the list.
The entire list, including the list items get re-created when a change is made to the countries list. Besides that this is sub-optimal, this can turn into the content flashing when the list items are more complex.
What should we pass to this "key" prop?
So, what should we pass into the "key" prop? We know that it should be a unique value, but where do we get this value?
The React docs give us two options:
- Data from a database
- Locally generated data (not generated on render!)
In our example this is easy. We add an id
property to every item in our countries list and we make sure to increment from the highest id
when we add a new country.
const defaultCountries = [
{ id: 1, name: "Argentina", capital: "Buenos Aires" },
{ id: 2, name: "Belgium", capital: "Brussels" },
{ id: 3, name: "The Netherlands", capital: "Amsterdam" },
{ id: 4, name: "Brazil", capital: "Brasília" },
{ id: 5, name: "Vietnam", capital: "Hanoi" },
];
function Countries() {
// ...
return (
// ...
<form
onSubmit={(event) => {
// ...
setCountries((prev) => [
...prev,
{ id: prev[prev.length - 1].id + 1, name, capital },
]);
// ...
}}
>
</form>
);
}
In a real world scenario where the countries data is retrieved from a backend you should discuss this with a backend engineer. They might require you to generate a unique id using the uuid
NPM package for example. The above won't work in your scenario? You could always try to create some unique data by combining one or more properties into a unique value.
Note: React will use the array index when no key
is passed to the component.
Now watch what happens to the list and list items in the inspector:
Only the list item that is interacted with will be created or removed from the DOM. Nice and easy optimization!
Note that all list item components are re-rendered on every change of the countries list. If you want to prevent this you can wrap the Item
component in React.memo.
Use the array index
Although, it is not recommended by the React docs in some cases you could get away with using the array index to the key
prop. To understand when, we should dive into what happens when we use the array index as key.
Let's remove the unique id again and use the array index for the key.
const defaultCountries = [
{ name: "Argentina", capital: "Buenos Aires" },
{ name: "Belgium", capital: "Brussels" },
{ name: "The Netherlands", capital: "Amsterdam" },
{ name: "Brazil", capital: "Brasília" },
{ name: "Vietnam", capital: "Hanoi" },
];
function Countries() {
// ...
return (
// ...
<ul>
{countries.map((country, index) => (
<Item
key={index}
// ...
/>
))}
</ul>
<form
onSubmit={(event) => {
// ...
setCountries((prev) => [...prev, { name, capital }]);
// ...
}}
>
</form>
);
}
Let's see what happens:
Exactly the same thing! Then why does React explicitly state that you should not use the array index as key? To show why, we need to add some state to the Item
component.
export function Item({ name, capital, onDelete }: ItemProps) {
const [isHighlighted, setIsHighlighted] = useState<boolean>(false);
return (
<li className="item">
<div className={`country${isHighlighted ? " country--highlight" : ""}`}>
<strong>{name}: </strong>
{capital}
</div>
<button
className="button"
type="button"
onClick={() => {
setIsHighlighted((currentState) => !currentState);
}}
>
Toggle highlight
</button>
<button
className="button"
type="button"
onClick={() => {
onDelete(name);
}}
>
Remove
</button>
</li>
);
}
Now, every country has a button that toggles a yellow background to the related list item when it is clicked. Now, the interesting part. Watch closely what happens when an item is deleted while a follow-up item is highlighted.
The highlight moves from The Netherlands to Brazil when Belgium is deleted. This is because we use the array index as keys. Deleting Belgium triggers the change of the value of the keys of all the following countries. Brazil will get the value that belonged to The Netherlands before.
React now compares the new keys to the previous render and identifies that the component with index 2
still exists. This means that for react the component with key value 2
is the same component as before the re-render. It will now update the component, but this time with different prop values (the values of Brazil). The Item
component will be re-rendered and not re-created. The state value will remain the same, which means that the component keeps the yellow background.
In this example it would be better to explicitly use a value that you know will be unique to the piece of data you want to show.
Bugs like this are hard to spot and can be hard to debug. This is why the React docs recommend using an id as the value for keys.
Is there a valid use case for using the array index?
There are use cases that exploit React's behaviour by using the array index as key.
If the list items:
- are pure components and...
- do not have any local state that is kept between renders
you could use the array index to improve performance for certain actions.
One use case that will benefit from this is when you want to replace all or a great part of the items in the list. Instead of React removing these components and creating the new components React will now re-render the affected components instead. Especially when you items are more complex components this could visually improve the performance of this part of your application.
Do I recommend this? It depends.. If a smooth experience is incredibly important to you, I think this is valid. However, I recommend leaving a comment behind explaining why you use the array index and how it improves performance. This prevents confusion when the next engineer passes this piece of code.
Conclusion
Now we know why React wants us to set a key when rendering dynamic lists of data and what to use as a value for such key.
If you want to play around with the code yourself I created a small repository with a working example. By default it uses the array index as key, so you can experience the behavior yourself. Check the repository here.
If you liked this article and want to read more make sure to check the my other articles. Feel free to contact me on Twitter with tips, feedback or questions!
Top comments (0)