React key attribute: best practices for performant lists
Nadia Makarevich
Posted on May 10, 2022
Originally published at https://www.developerway.com. The website has more articles like this đ
React âkeyâ attribute is probably one of the most âautopilotâ used features in React đ Who among us honestly can say that they use it because of ââŠsome valid reasonsâ, rather than âbecause eslint rule complained at meâ. And I suspect most people when faced with the question âwhy does React need âkeyâ attributeâ will answer something like âerrr⊠weâre supposed to put unique values there so that React can recognise list items, itâs better for performanceâ. And technically this answer is correct. Sometimes.
But what exactly does it mean ârecognise itemsâ? What will happen if I skip the âkeyâ attribute? Will the app blow up? What if I put a random string there? How unique the value should be? Can I just use arrayâs index values there? What are the implications of those choices? How exactly do any of them impact performance and why?
Letâs investigate together!
How does React key attribute work
First of all, before jumping into coding, letâs figure out the theory: what the âkeyâ attribute is and why React needs it.
In short, if the âkeyâ attribute is present, React uses it as a way to identify an element of the same type among its siblings during re-renders (see the docs: https://reactjs.org/docs/lists-and-keys.html and https://reactjs.org/docs/reconciliation.html#recursing-on-children).In other words, itâs needed only during re-renders and for neighbouring elements of the same type, i.e. flat lists (this is important!).
A simplified algorithm of the process during re-render looks like this:
- first, React will generate the âbeforeâ and âafterâ âsnapshotsâ of the elements
- second, it will try to identify those elements that already existed on the page, so that it can re-use them instead of creating them from scratch
- if the âkeyâ attribute exists, it will assume that items with the same âbeforeâ and âafterâ key are the same
- if the âkeyâ attribute doesnât exist, it will just use siblingâs indexes as the default âkeyâ
- third, it will:
- get rid of the items that existed in the âbeforeâ phase, but donât exist in the âafterâ (i.e. unmount them)
- create from scratch items that havenât existed in the âbeforeâ variant (i.e. mount them)
- update items that existed âbeforeâ and continue to exist âafterâ (i.e. re-render them)
Itâs much easier to understand when you play with code a little bit, so letâs do that as well.
Why random âkeyâ attributes are a bad idea?
Letâs implement a list of countries first. Weâll have an Item
component, that renders the countryâs info:
const Item = ({ country }) => {
return (
<button className="country-item">
<img src={country.flagUrl} />
{country.name}
</button>
);
};
and a CountriesList
component that renders the actual list:
const CountriesList = ({ countries }) => {
return (
<div>
{countries.map((country) => (
<Item country={country} />
))}
</div>
);
};
Now, I donât have the âkeyâ attribute on my items at the moment. So what will happen when the CountriesList
component re-renders?
- React will see that there is no âkeyâ there and fall back to using the
countries
arrayâs indexes as keys - our array hasnât changed, so all items will be identified as âalready existedâ, and the items will be re-rendered
Essentially, it will be no different than adding key={index}
to the Item
explicitly
countries.map((country, index) => <Item country={country} key={index} />);
In short: when CountriesList
component re-renders, every Item
will re-render as well. And if we wrap Item
in React.memo
, we even can get rid of those unnecessary re-renders and improve the performance of our list component.
Now the fun part: what if, instead of indexes, we add some random strings to the âkeyâ attribute?
countries.map((country, index) => <Item country={country} key={Math.random()} />);
In this case:
- on every re-render of
CountriesList
, React will re-generate the âkeyâ attributes - since the âkeyâ attribute is present, React will use it as a way to identify âexistingâ elements
- since all âkeyâ attributes will be new, all items âbeforeâ will be considered as âremovedâ, every
Item
will be considered as ânewâ, and React will unmount all items and mount them back again
In short: when CountriesList
component re-renders, every Item
will be destroyed and re-created from scratch.
And re-mounting of components is much, much more expensive, compared to the simple re-render when we talk about performance. Also, all performance improvements from wrapping items in React.memo
will go away - memoisation wonât work since items are re-created on every re-render.
Take a look at the above examples in the codesandbox. Click on buttons to re-render and pay attention to the console output. Throttle your CPU a little, and the delay when you click the button will be visible even with the naked eye!
How to throttle you CPU
In Chrome developer tools open âPerformanceâ tab, click the âcogwheelâ icon on the top right - it will open an additional panel, with âCPU throttlingâ as one of the options.
Why âindexâ as a âkeyâ attribute is not a good idea
By now it should be obvious, why we need stable âkeyâ attributes, that persist between re-renders. But what about arrayâs âindexâ? Even in the official docs, they are not recommended, with the reasoning that they can cause bugs and performance implications. But what exactly is happening that can cause such consequences when weâre using âindexâ instead of some unique id
?
First of all, we won't see any of this in the example above. All those bugs and performance implications only happen in âdynamicâ lists - lists, where the order or number of the items can change between re-renders. To imitate this, letâs implement sorting functionality for our list:
const CountriesList = ({ countries }) => {
// introduce some state
const [sort, setSort] = useState('asc');
// sort countries base on state value with lodash orderBy function
const sortedCountries = orderBy(countries, 'name', sort);
// add button that toggles state between 'asc' and 'desc'
const button = <button onClick={() => setSort(sort === 'asc' ? 'desc' : 'asc')}>toggle sorting: {sort}</button>;
return (
<div>
{button}
{sortedCountries.map((country) => (
<ItemMemo country={country} />
))}
</div>
);
};
Every time I click the button the arrayâs order is reversed. And Iâm going to implement the list in two variants, with country.id
as a key:
sortedCountries.map((country) => <ItemMemo country={country} key={country.id} />);
and arrayâs index
as a key:
sortedCountries.map((country, index) => <ItemMemo country={country} key={index} />);
And going to memoise Item
component right away for performance purposes:
const ItemMemo = React.memo(Item);
Here is the codesandbox with the full implementation. Click on the sorting buttons with throttled CPU, notice how "index"-based list is slightly slower, and pay attention to the console output: in the "index"-based list every item re-renders on every button click, even though Item
is memoised and technically shouldnât do that. The "id"-based implementation, exactly the same as âkeyâ-based except for the key value, doesnât have this problem: no items are re-rendered after the buttonâs click, and the console output is clean.
Why is this happening? The secret is the âkeyâ value of course:
- React generates âbeforeâ and âafterâ list of elements and tries to identify items that are âthe sameâ
- from Reactâs perspective, the âsameâ items are the items that have the same keys
- in âindexâ-based implementation, the first item in the array will always have
key="0"
, the second one will havekey="1"
, etc, etc - regardless of the sorting of the array
So, when React does the comparison, when it sees the item with the key="0"
in both âbeforeâ and âafterâ lists, it thinks that itâs exactly the same item, only with a different props value: country
value has changed after we reversed the array. And therefore it does what it should do for the same item: triggers its re-render cycle. And since it thinks that the country
prop value has changed, it will bypass the memo function, and trigger the actual itemâs re-render.
The id-based behaviour is correct and performant: items are recognized accurately, and every item is memoised, so no component is re-rendered.
This behaviour is going to be especially visible if we introduce some state to the Item component. Letâs, for example, change its background when itâs clicked:
const Item = ({ country }) => {
// add some state to capture whether the item is active or not
const [isActive, setIsActive] = useState(false);
// when the button is clicked - toggle the state
return (
<button className={`country-item ${isActive ? 'active' : ''}`} onClick={() => setIsActive(!isActive)}>
<img src={country.flagUrl} />
{country.name}
</button>
);
};
Take a look at the same codesandbox, only this time click on a few countries first, to trigger the background change, and only then click the âsortâ button.
The id-based list behaves exactly as youâd expect. But the index-based list now behaves funny: if I click on the first item in the list, and then click sort - the first item stays selected, regardless of the sorting. And this is the symptom of the behaviour described above: React thinks that the item with key="0"
(first item in the array) is exactly the same before and after the state change, so it re-uses the same component instance, keeps the state as it was (i.e. isActive
set to true
for this item), and just updates the props values (from the first country to the last country).
And exactly the same thing will happen, if instead of sorting weâll add an item at the start of the array: React will think that the item with key="0"
(first item) stays the same, and the last item is the new one. So if the first item is selected, in the index-based list the selection will stay at the first item, every item will re-render, and the âmountâ even will be triggered for the last item. In the id-based list, only the newly added item will be mounted and rendered, the rest will sit there quietly. Check it out in the codesandbox. Throttle your CPU, and the delay of adding a new item in the index-based list is yet again visible with the naked eye! The id-based list is blazing fast even with the 6x CPU throttle.
Why âindexâ as a âkeyâ attribute IS a good idea
After the previous sections itâs easy to say âjust always use a unique item id
for âkeyâ attributeâ, isnât it? And for most cases itâs true and if you use id
all the time nobody will probably notice or mind. But when you have the knowledge, you have superpowers. Now, since we know what exactly is happening when React renders lists, we can cheat and make some lists even faster with index
instead of id
.
A typical scenario: paginated list. You have a limited number of items in a list, you click on a button - and you want to show different items of the same type in the same size list. If you go with key="id"
approach, then every time you change the page youâll load completely new set of items with completely different ids. Which means React wonât be able to find any âexistingâ items, unmount the entire list, and mount completely fresh set of items. But! If you go with key="index"
approach, React will think that all the items on the new âpageâ already existed, and will just update those items with the fresh data, leaving the actual components mounted. This is going to be visibly faster even on relatively small data sets, if item components are complicated.
Take a look at this example in the codesandbox. Pay attention to the console output - when you switch pages in the âid"-based list on the right, every item is re-mounted. But in âindex"-based list on the left items are only re-rendered. Much faster! With throttled CPU, even with 50 items very simple list (just a text and an image), the difference between switching pages in the âid"-based list and âindex"-based list is already visible.
And exactly the same situation is going to be with all sorts of dynamic list-like data, where you replace your existing items with the new data set while preserving the list-like appearance: autocomplete components, google-like search pages, paginated tables. Just would need to be mindful about introducing state in those items: they would have to be either stateless, or state should be synced with props.
All the keys are in the right places!
That is all for today! Hope you liked the read and have a better understanding now of how React âkeyâ attribute works, how to use it correctly, and even how to bend its rules to your will and cheat your way through the performance game.
A few key takeaways to leave with:
- never use random value in the âkeyâ attribute: it will cause the item to re-mount on every render. Unless of course, this is your intention
- there is no harm in using the arrayâs index as âkeyâ in âstaticâ lists - those whose items number and order stay the same
- use item unique identifier (âidâ) as âkeyâ when the list can be re-sorted or items can be added in random places
- you can use the arrayâs index as âkeyâ for dynamic lists with stateless items, where items are replaced with the new ones - paginated lists, search and autocomplete results and the like. This will improve the listâs performance.
Have a great day, and may your list items never re-render unless you explicitly told them so! âđŒ
...
Originally published at https://www.developerway.com. The website has more articles like this đ
Subscribe to the newsletter, connect on LinkedIn or follow on Twitter to get notified as soon as the next article comes out.
Posted on May 10, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
May 16, 2022