Deep dive into React keys bugs
Tags: react, performance
Published: 2022-02-14 11:50:22

---
title: Deep dive into React keys bugs
description: Let's figure out what are React keys and why do we need them
published: true
tags: react, performance
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ulafze6qqvwzgdp9char.png
---

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?"

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

![Diff example 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/43g2pp3nlvue11setzbf.png)
 
Computing this diff won't be that hard, but what happens if we shuffle the new list?

![Diff example 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v6pnkl3rr7z00gudn5lu.png)

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.

![Diff keys hints](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1gsn07de361kwg66dvat.png)
 
So what bad can happen if we ignore or misuse these keys?

### Case 1. Performance issues

[Here](https://stackblitz.com/edit/react-ts-ako74b?devtoolsheight=33&file=Hello.tsx) 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.

```tsx
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**

![Case 1 ok](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z09oolblsematgsrcssv.png)
 
As you may expect, there are just new components.

**Example 2. Add items to the start of the list, don't use keys**

![case 1 bug](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d5fs36888ceubwx1tywr.png)
  
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**

![case 1 fix](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3f1g25fpkd9nrmghnhsn.png)
 
It works perfectly, no unneeded updates, React know exactly where each component moved.

### Case 2. Bugs with inputs

[Here](https://stackblitz.com/edit/react-ts-jhue1z?file=Hello.tsx) 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.

```tsx
{items.map((item) => (
	<Item item={item} onUpdate={handleUpdate} onDelete={handleDelete} />
))}
```

And each item is just an input with a control button.

```tsx
let Item = ({ item, onUpdate, onDelete }) => {
	// ...
	
	return (
		<div>
			<input
				defaultValue={item.title}
				placeholder="Item #..."
				onChange={handleChange}
			/>
			&nbsp;
			<button onClick={handleDelete}>x</button>
		</div>
	);
};
```

Also, there is a dump of an inner state down on the page

```tsx
{JSON.stringify(items, null, 2)}
```

**Example1. Create a few items and delete the first one, don't use any keys.**

Before deletion:
![case 2 ok](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r8j1y4lsdrzt0r7v9b8e.png) 

After deletion:
![case 2 bug](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j055u2p1um392lmazzbd.png) 

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](https://reactjs.org/docs/reconciliation.html#dom-elements-of-the-same-type)), but just updates the property.

**Example 2. Create a few items and delete the first one, use ID as a key.**

![case 2 fix](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t484bdeqifxxn3tju3gj.png) 

As expected, everything works fine here.

### Case 3. Bugs with effects & DOM manipulations

[Here](https://stackblitz.com/edit/react-ts-k1xg3x?file=Hello.tsx) 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.

```jsx
// 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.**

![case 3 bug](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/grpnukrvt9gswl6guld7.gif)
  
🐞 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.**

![case 3 fix](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/841uxu2y1j882ocpxn5s.gif) 

It works!

### Case 4. Bugs with animations

[Here](https://stackblitz.com/edit/react-ts-xpgzjp?file=Hello.tsx) 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? 

```css
@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.**

![case 4 ok](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2jqywa7vsfhfr583g3n9.gif) 

Looks ok to me.

**Example 2. Add new item to the start, don't use any keys.**

![case 4 bug](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/33jvqvrl968zzbfrv80k.gif)
 

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

![case 4 fix](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7658ihb592atgukqonxo.gif) 

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:

```jsx
<ul>{['qwe', 'asd', 'zxc'].map(item => (<li>{item}</li>))}</ul>
```

[Here](https://stackblitz.com/edit/react-ts-tgthiy?file=Hello.tsx) 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.

![console warning](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e46vyhhtd6lr7o6r8pac.png)

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

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

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

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

![multiple updates](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wgdxbj3gx30156z581xt.png)

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

```jsx
let addKeysToArray = (array) =>
  array.map((item) => ({
    key: generateKey(),
    value: item,
  }));

let array = ['qwe', 'asd', 'zxc']
let arrayWithKeys = addKeysToArray(array)
console.log(arrayWithKeys)
```
 
![array with keys example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lfneyhu546sy4q11bz0f.png) 


### 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](https://cv.kozlovzxc.ru/) 👋