A few days ago, I was casually browsing open positions and one job application had a quick question: "What is wrong with this React code?"
<ul>{['qwe', 'asd', 'zxc'].map(item => (<li>{item}</li>))}</ul>
Quick answer would be that it's missing key
property, but at this moment I caught myself on the feeling that I don't deeply understand what are React keys and what can go wrong if we use it incorrectly. Let's figure it out together!
β Stop here for a moment, can you come up with an actual bug caused by misusing React keys? Please share your example in the comments!
What are React keys anyway
This will be a bit of simplified explanation, but it should be enough to dive into examples.
When we have some previous inner state and the new inner state, we want to calculate the difference between them, so we can update them DOM to represent the new inner state.
diff = new_state - old_state
new_dom = old_dom + diff
Let's take a look at this example, there is a list of items, and we are adding new item to the bottom of the list.
Computing this diff won't be that hard, but what happens if we shuffle the new list?
Computing diff over these changes suddenly isn't that easy, especially when there are children down the tree. We need to compare each item with each to figure out where something moved.
Keys for the rescue! Basically with keys you are hinting to React where all items moved in this shuffle, so it doesn't need to calculate it itself. It can just take existing items and put them in the right place.
So what bad can happen if we ignore or misuse these keys?
Case 1. Performance issues
Here is the simple app if you want to play with it yourself.
We can use a simple component which just logs if props were updated.
let Item: FC<any> = ({ item }) => {
let [prevItem, setPrevItem] = useState(undefined);
useEffect(() => {
console.log('On update', item, prevItem);
setPrevItem(item);
}, [item]);
return <div>{item.title}</div>;
};
Example 1. Add items to the end of the list, don't use keys
As you may expect, there are just new components.
Example 2. Add items to the start of the list, don't use keys
Things aren't going as expected here, there are n
updates on each click where n
is the number of items in the list. On each new item, all items shift to the next component, which may be a bit confusing at first.
Take another look at the console log here again.
Example 3 & 4. Add items anywhere, use ID as a key
It works perfectly, no unneeded updates, React know exactly where each component moved.
Case 2. Bugs with inputs
Here is the simple app if you want to play with it yourself.
The issue with keys in this example is that if you don't re-create DOM elements because of incorrect React keys, these elements can keep user input, when underlying data was changed.
In this example, there is just a list of Items.
{items.map((item) => (
<Item item={item} onUpdate={handleUpdate} onDelete={handleDelete} />
))}
And each item is just an input with a control button.
let Item = ({ item, onUpdate, onDelete }) => {
// ...
return (
<div>
<input
defaultValue={item.title}
placeholder="Item #..."
onChange={handleChange}
/>
<button onClick={handleDelete}>x</button>
</div>
);
};
Also, there is a dump of an inner state down on the page
{JSON.stringify(items, null, 2)}
Example1. Create a few items and delete the first one, don't use any keys.
As you see, inner state got unsynchronized with DOM state, because inner models shifted as in the first example, but view stayed the same.
This happens because React doesn't actually recreate an element of the same type (docs), but just updates the property.
Example 2. Create a few items and delete the first one, use ID as a key.
As expected, everything works fine here.
Case 3. Bugs with effects & DOM manipulations
Here is the simple app if you want to play with it yourself.
The fun part is that React keys isn't only about lists, they may be used with singe item as well.
Let's imagine that we have a task to show some notifications for users for 5 seconds, e.g. these are some "π° Deals π€".
Some straightforward implementation when you just hide this box when timer fires.
// We want this message to disapear in 5 seconds
let Notification = ({ message }) => {
let ref = useRef<HTMLDivElement | null>(null);
useEffect(() => {
setTimeout(() => {
if (ref.current != null) {
ref.current.style.display = 'none';
}
}, 5000);
}, [message]);
return <div ref={ref}>{message}</div>;
};
Example 1. Generate notification, wait a bit, generate again.
π Nothing happens if we try to generate another notification.
This is because React doesn't re-create the component just because of an updated property, it expects the component to handle this on its own.
Example 2. Generate notification, wait a bit, generate again, but use message as a key.
It works!
Case 4. Bugs with animations
Here is the simple app if you want to play with it yourself.
What if we want to somehow highlight newly created items in our fancy to-do list?
@keyframes fade {
from {
color: red;
opacity: 0;
}
to {
color: inherit;
opacity: 1;
}
}
.item {
animation: fade 1s;
}
Example 1. Add new item to the end, don't use any keys.
Looks ok to me.
Example 2. Add new item to the start, don't use any keys.
Something is off, we are adding items to the start, but the last item is highlighted.
This happens again because React shifts inner models, same issue as for bug with inputs.
Example 3. Add new item to the start, use ID as a key.
Everything works perfectly.
Final notes
So as we figured out, React keys aren't something magical, they are just hinting React if we need to re-create or update some component.
As for the initial question:
<ul>{['qwe', 'asd', 'zxc'].map(item => (<li>{item}</li>))}</ul>
Here is the stup where you can try all solutions.
Solution 1: Do nothing.
In this concrete example, this list should work just fine because there are just 3 items, and you don't update them, but it won't be as much performant and there will be an annoying warning in the console.
Solution 2: Item as a key.
If you are sure that this list have only unique values, e.g. contact information, you can use these values as keys.
<ul>
{['qwe', 'asd', 'zxc'].map((item) => (
<li key={item}>{item}</li>
))}
</ul>
Solution 3: Index as a key.
If you are sure that this list never changes by user or anyone else except by the developer, you can use index as a key.
<ul>
{['qwe', 'asd', 'zxc'].map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
Be careful using indexes as keys because in all examples above you can set keys as indexes and all bugs will persist.
Solution 4: Generated keys.
You also can try to generate the keys.
let generateKey = () => {
console.log('Generating key...');
return Math.trunc(Math.random() * 1000).toString();
};
/// ...
<ul>
{['qwe', 'asd', 'zxc'].map((item) => (
<li key={generateKey()}>{item}</li>
))}
</ul>
In this case, you need to consider that these keys will be generated every time you update the component's state.
Solution 5: Keys which are generated once
To solve previous issue you need to move this array somewhere outside of a React component and generate keys manually.
let addKeysToArray = (array) =>
array.map((item) => ({
key: generateKey(),
value: item,
}));
let array = ['qwe', 'asd', 'zxc']
let arrayWithKeys = addKeysToArray(array)
console.log(arrayWithKeys)
References
- https://reactjs.org/docs/lists-and-keys.html
- https://reactjs.org/docs/reconciliation.html
- https://blog.logrocket.com/unfavorable-react-keys-unpredictable-behavior/
- https://kentcdodds.com/blog/understanding-reacts-key-prop
- https://habr.com/ru/company/hh/blog/352150/ (π·πΊ Russian)
p.s.: I'm looking for a remote senior frontend developer position, so if you are hiring or if you can reference me, please take a look on my cv π
Top comments (1)
Great post π₯
I really feel like the key prop is the thing you think you know but not really.
I too wrote about unique use cases of the the key prop π