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.
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.
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.