<Niek/>

Arrow downAll posts

Why do we need to set a "key" prop in lists in React?

10 Jan 2024

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.

A screenshot of the error React logs in the browser console when you do not pass a "key" prop to your list items. It read: "Warning: Each child in a list should have a unique "key" prop.".
Big chance that you have seen this error before..

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.

A screen recording of a browser window with the inspector open. It shows a list of countries and its capital. After each country is a "Remove" located and underneath the list is a small form to add a new country and capital. The recording first removes one country and then adds a new country. The inspector panel show that the entire list including its list items are re-created.
Watch closely to what happens to the list items in the inspector panel.

The entire list, including the list items get re-created when a change is made to the countries list. Besides that this is suboptimal, 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:

  1. Data from a database
  2. 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:

Exactly the same screen recording as the previous gif, but now only the list item that gets changed is created or removed from the DOM.
Again, watch the list and list items in the inspector panel closely

Only the list item that is interacted with will be created or removed from the DOM. Nice and easy optimisation!

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:

Another screen recording. Exactly the same as the previous one.
Again, watch the list and items in the inspector panel. Do you see a difference?

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.

A similar screen recording. Now first The Netherlands is highlighted and then Belgium is removed. The Netherlands is the third country in the list, while Belgium is the second item in the list. After removing Belgium the highlight moves from The Netherlands to Brazil (the fourth item in the original list).
Notice that after removing Belgium the yellow background moves from The Netherlands to Brazil.

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 behaviour 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!