If you've done any UI development, you've probably had to build a list or two.
What do I mean by a list?
Let's take Twitter as an example.
When you log onto Twitter, you're presented with a feed that contains an endless list of Tweets. In other words, a list is a series of similarly structured content that's placed one after the other.
In this post, we'll take a look at how to build lists effectively in React.
The naive approach would be to hardcode each element in the list:
function Feed() {
return (
<div>
<span>I am tweet 1</span>
<span>I am tweet 2</span>
<span>I am tweet 3</span>
<span>I am tweet 4</span>
</div>
);
}
This approach has a few problems.
Duplicate code is error-prone and hard to read
The example above is simple.
You can add a new Tweet by creating a new <span />
element. But this isn't a real-world scenario. What do things look like when the JSX is a little more complex?
function Feed() {
return (
<div>
<div>
<span>@username</span>
<span>I am tweet 1</span>
<span>created timestamp</span>
<div>
<span>Like</span>
<span>Retweet</span>
</div>
</div>
<div>
<span>@username</span>
<span>I am tweet 2</span>
<span>created timestamp</span>
<div>
<span>Like</span>
<span>Retweet</span>
</div>
</div>
<div>
<span>@username</span>
<span>I am tweet 3</span>
<span>created timestamp</span>
<div>
<span>Like</span>
<span>Retweet</span>
</div>
</div>
<div>
<span>@username</span>
<span>I am tweet 4</span>
<span>created timestamp</span>
<div>
<span>Like</span>
<span>Retweet</span>
</div>
</div>
</div>
);
}
This code is a mess...and we're only 4 Tweets in!
Now if you try to copy the code to make a new Tweet, you're more likely to miss something. This code is also harder to maintain.
You can't hardcode JSX for dynamic data
It's clear the code above is too messy, but there's a more serious problem.
You may have already realized it, but for a website like Twitter, it's not possible to hardcode the JSX. Tweets are created dynamically. You won't have the information ahead of time to write the JSX.
Instead, the JSX is created on the fly from Tweets fetched from a database.
Using dynamic lists will solve both problems.
Great!
But how do you do that?
React doesn't have any built-in utilities to create lists. Instead, it's recommended you leverage standard JavaScript array methods. The most common method used is the map method.
But before we get to that, it's important to understand how React renders arrays:
Notice how the children
passed to the div
is an array that contains two JSX elements. React takes that array and dumps the children out in the browser. This lets us do some powerful things using array methods.
Map an array of data to JSX
With the map
method, you can convert array elements into JSX. React can then take that array and render things properly:
In the example above, we map over a collection of Tweet data and return a new collection of JSX.
This code isn't duplicated, so it's easier to read and maintain. Another benefit is that we can load the Tweets dynamically from a database. As long as the Tweets are in the correct format, the code doesn't care how you get them.
Each child in a React list must have a unique key
If you take a look at the console in the example above, you'll see that React is throwing an error:
Warning: Each child in a list should have a unique "key" prop.
A key is a unique string or number.
React uses keys to correctly identify elements in an array. The elements are tracked so that React can make DOM updates (i.e. re-renders) efficiently. A bad key can lead to bugs when a list can be reordered (e.g. via sorting) or when elements can be added/removed from a list.
Let's look at a bug in action.
In the example below, we have two lists of boxes. Each box has an HTML input
element and a number to identify it. Right above the boxes is a Shuffle button that re-orders the boxes.
The boxes in list one are using a unique key. The boxes in list two are using the array index for the keys. Since an array index isn't unique to a box, we've introduced a subtle bug when we try to shuffle the boxes.
Try it out for yourself and see if you can spot the issue.
The first thing to note is that the order of the boxes is changing in both lists. The issue is that only the first list is keeping track of the input
elements correctly. Since you can't use an array index to uniquely identify a box, React can't update the DOM correctly.
How do you pick a good key?
Here are some basic things to keep in mind.
Choose a key based on your data source
Data from a database → DB IDs
Data locally →crypto.randomUUID()
Choose a unique key
A key must be unique so that React can differentiate between elements. While this must hold for sibling elements (i.e. elements in the same array), there's no requirement for keys to be unique across an entire app:
function List() {
const items = [1, 2, 3];
return (
<>
<div>
List One
{items.map(item => <span key={item}>{item}</span>)}
</div>
<div>
List Two
{items.map(item => <span key={item}>{item}</span>)}
</div>
</>
);
}
The keys in List One and List Two aren't unique.
But that's not a problem because they're separate lists. As long as the keys are unique within a single list, React can handle things correctly.
Keys must be stable
React needs to be able to consistently identify elements. This isn't possible if the keys you use aren't stable. This means that your keys can't change.
function List() {
const items = [1, 2, 3];
return (
<div>
{items.map(item => (
<span key={Math.random()}>{item}</span>
))}
</div>
);
}
In this example, Math.random
ensures that we have unique keys. The issue is that the keys aren't stable. Every time the List
component re-renders, Math.random
runs again, and the keys change.
From React's perspective, these are different elements.
All React can do is remove the elements and recreate them. Compared to updating the elements in place, this is a more expensive operation.
Keys should go on top-level elements
When you map an array of data to an array of elements, it's important to put the key on the outermost element. React won't traverse the component hierarchy to find a key, so it won't be able to identify elements correctly.
function List() {
const items = [1, 2, 3];
// Wrong
const listOne = items.map(item => (
<div>
<span key={item}>{item}</span>
</div>
));
// Right
const listTwo = items.map(item => (
<div key={item}>
<span>{item}</span>
</div>
));
// ...render
}
The key has to go on the div
instead of the span
so that React can use it.