Everything you need to know about the 'key' prop in React

Key to your success

A common practice rendering a list or sequence of components in React is to use a map:

const List = () => {
    const listItems = ['Item 1', 'Item 2', 'Item 3'];

    return (
        <ul>
            {listItems.map(item) => <li>{item}</li>}
        </ul>
    );
}

Well, we now see the list of our items, but what is this warning in our console?

React key warning

Well, it appears that we need to pass a special unique key prop to each child element. Fine, we know that the map method of Array accepts index argument in its callback. So we can simply do the following to fix the warning.

const List = () => {
    const listItems = ['Item 1', 'Item 2', 'Item 3'];

    return (
        <ul>
            {listItems.map((item, index)) => <li key={index}>{item}</li>}
        </ul>
    );
}

And whoa - the warning is now gone. But is it the right solution? (Spoiler: the answer is no). Let’s discuss why, how to handle key props in a proper way.

It’s all about optimization

React is a powerful JavaScript library for building user interfaces. React itself has a lot of optimization techniques under the hood, including Virtual DOM.

Every time the view is changed, Virtual DOM compares the previous version of the DOM representation with the new one. This process is called ‘diffing’. According to React documentation, this algorithm is based on two assumptions:

  1. Two elements of different types will produce different trees.
  2. The developer can hint at which child elements may be stable across different renders with a key prop.

I will skip how diffing algorithm works for elements of different types, for now, let’s focus on our key.

Let’s imagine that we are machines with a special comparing algorithm and we need to compare two UI states, find the difference and update only those parts which are different:

Our old version of UI state

<ul>
  <li>Item 1</li>
  <li>Item 2</li>
</ul>

The new version of UI state

<ul>
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
</ul>

Well, pretty easy, right? We compare two <li>Item 1</li> trees and two <li>Item 2</li> trees. That’s a match, so we don’t need any updates here. We don’t have the <li>Item 3</li> tree, so we insert it. Nice!

All right then, the new update is pending our comparison:

Our current UI state

<ul>
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
</ul>

And the upcoming UI state:

<ul>
  <li>Item 4</li>
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
</ul>

Remember: we are just machines and we can’t understand that Item 1, Item 2, and Item 3 are still there. We are running our comparing algorithm: <li>Item 1</li> tree does not match the <li>Item 4</li>. Same goes for others, <li>Item 2</li>, <li>Item 1</li> etc. So we end up updating the whole tree. Not really an optimized way, right?

That’s where the key prop comes into play!

Let’s use the key to match children in the original tree with children in the subsequent tree:

<ul>
  <li key="first">Item 1</li>
  <li key="second">Item 2</li>
  <li key="third">Item 3</li>
</ul>

now each child has key

<ul>
  <li key="fourth">Item 4</li>
  <li key="first">Item 1</li>
  <li key="second">Item 2</li>
  <li key="third">Item 3</li>
</ul>

So now instead of comparing each child element step-by-step, we simply associate a key with each item. And now we know that items with keys first, second and third have just moved and the element with key fourth is the new one. Neat!

So I guess now you understand why we need a key prop and why we can’t use item index as a key. Using an item’s index in the array as a key only works well if the items are never reordered.

In practice, finding a key is usually not hard. The element you are going to display may already have a unique ID, so the key can just come from your data:

<li key={item.id}>{item.name}</li>

But that’s not all about keys!

There is a little trick with the key prop. It isn’t used a lot, but understanding this principle will help you understand React a bit better. It has to do with React component “instances” and how React treats the key prop.

React’s key prop gives you the ability to control component instances. Each time React renders your components, it’s calling your functions to retrieve the new React elements that it uses to update the DOM. If you return the same element types, it keeps those components/DOM nodes around, even if all the props changed.

The only exception to this is the key prop. This allows you to return the exact same element type, but force React to unmount the previous instance and mount a new one. This means that all state that had existed in the component at the time is completely removed and the component is “reinitialized” for all intents and purposes. For function components, this means that React will run cleanup on effects, then it will run state initializers and effect callbacks.

NOTE: effect cleanup actually happens after the new component has been mounted, but before the next effect callback is run.

For class components, React will run componentWillUnmount, constructor, and componentDidMount.

const List = () => {

  console.log('List called');
  
  const [listItems, setListItems] = React.useState(() => {
    console.log('List useState initializer');
    return [1, 2, 3];
  });

  const addItem = () => setListItems(
    items => [...items, items[items.length - 1] + 1]
  );

  React.useEffect(() => {
    console.log('List useEffect callback');

    return () => {
      console.log('List useEffect cleanup');
    }
  }, [])

  console.log('List returning react elements');

  return (
    <>
      <ul>
        {listItems.map(item => <li key={item}>{`Item ${item}`}</li>}
      </ul>
      <button onClick={addItem}>Add Item</button>
    </>
  )
}

function ListParent() {
  // using useReducer to ensure that any time you call
  // setListKey, the `listKey` is set to a new object
  const [listKey, setListKey] = React.useReducer(c => c + 1, 0);

  return (
    <div>
      <button onClick={setListKey}>reset</button>
      <List key={listKey} />
    </div>
  )
}

Here’s what we will see in the console after clicking the ‘Add item’ button and then clicking reset:

// getting mounted

List called

List useState initializer

List returning react elements

// now it's mounted

List useEffect callback

// click the counter button

List called

List returning react elements

// notice the initializer and effect callback are not called this time

// click the reset button in the parent

// these next logs are happening for our new instance

List called

List useState initializer

List returning react elements

// cleanup old instance

List useEffect cleanup

// new instance is now mounted

List useEffect callback

Conclusion

The key prop isn’t just for getting rid of that annoying React console error when you try to render an array of elements (all “annoying” errors from React are awesome and help you avoid bugs, so please do not ignore them). The key prop can also be a useful mechanism for controlling React component and element instances.

Comments