Engineering

Proper usage of keys in React lists

Headshot, Winggo Tse
By Winggo Tse

Why are keys necessary when building lists in React? How does React use it under the hood? In this blog post, I'll be covering those questions so you don't need to wonder any longer.

Arrow iconBack to all articles
Cover image

I've worked with React for over two years now as a full stack developer. Over the course of this time, I've worked with lists in various contexts, such as constructing a timeline for notifications, building a dropdown menu, and creating an editable list of names & emails. I know all lists in React require the key prop, but I've never looked into why that is the case until recently.

Where the problem began

A couple weeks ago I was building a component that lets users reorder their files using the react-sortable-hoc npm package. I called the component SortableFileList and it seemed pretty straightforward. The code looks like this:

class SortableFileList extends React.Component {
  render () {
    const { files } = this.props
    return (
      <SortableList
        className="sortable-file-list"
        onSortEnd={...}
      >
        {files.map((file, index) => (
          <SortableElement key={index}>
            {file.name}
          </SortableElement>	
        ))}
      </SortableList>
    )
  }	
}

Let's get rid of the annoying 'Each child in a list should have a unique “key" prop' warning by using index as key

As you can see, I'm using the index of the list items as keys. It's intuitive to use the index because it's unique for each list item and is simple to use. Things worked fine until I started adding and reordering items.

Buggy sortable list What is happening??

I looked everywhere to determine the source of the bug. Was it coming from the sortable list npm package? Or is it due to my sorting algorithm? It seemed like I ruled everything out, until I began looking into the key prop.

The key prop. What's it for?

According to React docs, keys give elements a stable identity so React can determine which items have changed, are added, or are removed. Keys should be permanent and unique among its siblings. If my list was static, using indexes as keys would've been fine. In this case however, actions such as sorting, adding, and removing are involved which makes using index as key an anti-pattern.

Let's take a deep dive at how React uses key under the hood. Imagine a simple list that renders twice, with the second render adding an item:

<!-- Render 1 -->
<ul>
	<li>made</li>
	<li>easy</li>
</ul>

<!-- Render 2 after adding an item to the list -->
<ul>
	<li>paperwork</li>
	<li>made</li>
	<li>easy</li>
</ul>

Without keys, React will iterate over the children of the two lists in lockstep and in sequence from first to last while comparing the before and after values of the children. React will compare 'made' with 'paperwork', 'easy' with 'made', and see the addition of <li>easy</li> as a third list item. As a result, React will trigger three changes to the DOM tree: replacing <li>made</li> with <li>paperwork</li>, <li>easy</li> with <li>made</li>, and adding <li>easy</li>.

When keys are used however, React will use the information passed by the key prop to compare more efficiently. Let's look at the same HTML but with keys:

<!-- Render 1 -->
<ul>
	<li key="item2">made</li>
	<li key="item3">easy</li>
</ul>

<!-- Render 2 after adding an item to the list -->
<ul>
	<li key="item1">paperwork</li>
	<li key="item2">made</li>
	<li key="item3">easy</li>
</ul>

Instead of comparing children from first to last, React will compare using matching keys between the before and after DOM nodes. <li key="item2">made</li> from the before list is compared to <li key="item2">made</li> from the after list, and so forth. Using this approach, only one change is observed, thus triggering only one change in the DOM three. Rerender time is reduced drastically and unexpected component states are prevented.

The docs also state that React defaults to using indexes as keys if no explicit key prop is passed in. Might as well drop specifying key={index} if that were to happen anyways!

My solution

List indexes as keys is not a good solution because they do not provide a stable identity. List indexes change depending on the size and order of the list. Instead, keys should be sourced from a permanent and unique property of a list item. In my example, using file.id would be the perfect solution.

<SortableList
  className="sortable-file-list"
  onSortEnd={...}
>
  {files.map((file, index) => (
    <SortableElement key={file.id}>
      {file.name}
    </SortableElement>	
  ))}
</SortableList>

file.id is permanent and unique to each list item

Here is the format of each file:

SortableFileList.propTypes = {
	files: PropTypes.arrayOf(
		PropTypes.shape({
			id: PropTypes.string.isRequired,
			name: PropTypes.string.isRequired,
		})
	).isRequired,
}

And tada! This one line fix of using the file.id as key fixed everything.

Fixed sortable list Bug free!

Workarounds

But what if my files don't have an id property? Is there an alternate property that meets the permanent and unique criteria for a key? My files could very much look like this instead:

SortableFileList.propTypes = {
	files: PropTypes.arrayOf(
		PropTypes.shape({
			name: PropTypes.string.isRequired,
			createdAt: PropTypes.string.isRequired,
		})
	).isRequired,
}

Both name and createdAt are permanent properties, but are not necessarily unique to items in a list. Here we can artificially create a unique identifier.

let fileIdCounter = 1

function createFilesWithIds (files) {
	return files.map((file) => ({
		id: fileIdCounter++,
		name: file.name,
		createdAt: file.createdAt,
  }))
}

class SortableFileList extends React.Component {
  render () {
    const { files } = this.props
    const filesWithIds = createFilesWithIds(files)
    return (
      <SortableList
        className="sortable-file-list"
        onSortEnd={...}
      >
        {filesWithIds.map((file, index) => (
          <SortableElement key={file.id}>
            {file.name} - {file.createdAt}
          </SortableElement>	
        ))}
      </SortableList>
    )
  }	
}

Workaround #1 - create my own own ids

The second workaround is to let a third party library handle id generation instead of implementing one yourself. Some id generation npm libraries that come to mind are nanoid and uuid.

import { nanoid } from 'nanoid'

function createFilesWithIds (files) {
	return files.map((file) => ({
		id: nanoid(),
		name: file.name,
		createdAt: file.createdAt,
  }))
}

Workaround #2 - let third party library handle ID generation

Summary

We've covered a real example of how improper usage of list keys can lead to app-breaking bugs, inspected closely into how React uses keys to efficiently rerender the DOM tree under the hood, explored how I solved my sortable list issue, and looked into workarounds for generating suitable list keys when clear unique identifiers are unavailable. By implementing these best practices into your code, you can save time wrestling with bugs and optimize your app performance.

We've applied these practices to our code at Anvil and believe sharing our experiences helps everyone in creating awesome products. If you're developing something neat with PDFs or paperwork automation, let us know at developers@useanvil.com. We'd love to hear from you.