How to avoid weird issues when using keys in React

Find out how to avoid the common anti-patterns

Ever since I first started working with React.js, I have been running into weird issues and bugs I had never faced when working with Vanilla JS.

Well, I have to admit that React has definitely revolutionised the development experience while working with the frontend part of a web app. The very concept of Virtual DOM was introduced (or at least popularized) by Facebook when releasing React. And while it makes your code a lot more beautiful, maintainable, and cleaner than before, you can easily run into bugs if you do not completely understand how the virtual DOM works. This forms the very foundation of your understanding of React, and while I try to explain a little of that to you, maybe grab a cup of coffee and follow along.

What on Earth is Virtual DOM

As per React's official documentation:

The virtual DOM (VDOM) is a programming concept where an ideal, or “virtual”, representation of a UI is kept in memory and synced with the “real” DOM by a library such as ReactDOM. This process is called reconciliation.

Virtual DOM is what makes the declarative nature of React possible. By declarative nature I mean that we only specify what exactly we want the application state to be on a particular event, we do not go on modifying the DOM manually to achieve that state (unlike Angular, Jquery etc). Let me make that clear by using an example. Suppose we want to turn the heading of the page to "Success" when clicking a particular button. First, we code the same with Jquery:

$(document).ready(function() {
  $("button.check").click(function() {
    $("h1").text("Success");
  });
});

When this script is embedded inside an HTML such as below (showing only the body) :

<div>
  <h1>Click the below button to check if you're awake!<h1>
  <button type="button" class="check">Click me!</button>
</div>

This works well for you. Let's see how we code the same functionality with React (in JSX):

const SomeComponent = () => {
  const [btnClicked, setBtnClicked] = useState(false);

  const onClickBtn = () => setBtnClicked(true);

   return (
     <div>
       <h1>
          {
             btnClicked ?
             "Success"
             : "Click the below button to check if you're awake!"
          }
      </h1>
       <button type="button" onClick={onClickBtn}>Click me!</button>
     </div>
   );
};

Did you notice the difference between the two approaches? In the first, you tell the browser step by step what to do when something happened. You wrote the "How's" of the process. You described how it will happen as you did it manually. You are interacting with DOM yourself.

In contrast, the second approach with React just tells what to display as text in the h1 tag, in the two cases. You did not write how it will happen. You did not actually interact with the DOM. This is how React works, here the virtual DOM comes into the picture.

The virtual DOM just creates a tree-like structure of your HTML elements, similar to a DOM. A logical question would be: why do we need double work? The answer is: Frequent updates to the real DOM are very computation-intensive (because of the repainting of all the UI that needs to be done). So manipulating the DOM directly is going to be performance heavy. With React, the state changes user triggers are first applied to the virtual DOM. The new and previous versions of the V-DOM are compared by an algorithm called the diffing algorithm and only the required minimal updates are applied to the real DOM as batch updates. This gives the much-needed performance optimisation.

Cool, but then how things go wrong

We do not properly understand (or implement) the keys.

Keys were made for some purpose. They help you avoid unnecessary re-rendering of the UI components. If the key for a node has not changed, React won't re-render it every time there's a side-effect.

If we miss adding keys to the elements rendered in a list, for example, React issues a warning in your console. That is because if you don't add the keys, and say your list has thousands of elements you can scroll through, your performance will surely go down even if one more element is added/removed by the user in the list. Because that's a list and any changes to the list will re-render the entire list. 😞

Thankfully, this can be avoided when we pass the keys to the elements 😀. On adding a new element, the keys of the existing elements won't change, and won't be re-rendered too, so you will save extra unnecessary renders.


This all seems quite well until we start to think what to use as keys for the elements in the list.

Imagine if you used the element index as the key. When you re-order the lists (adding a new element in between or removing one) it results in weird behaviour, like adding a new element in between causes an element to be added at the end of the list. This means choosing element index as the key is not a good idea at least when the reordering of elements is possible. We should use a valid object id as the key, that remains unique to its element every time and cannot be changed on reordering.

However, in case you got a static list that cannot be re-ordered, and your elements got no unique ids, you may use the indices as the keys.


Another common mistake is using random keys for the elements, such as ones generated by Math.random(); or uuid(). Using such a key means every time there's a re-render there will be a different key for your element. This results in wasted renders.

Also, an important thing to remember is: The "key" is not passed down as a prop to any component. So just in case you wanted to have the value passed as key also be passed as a prop to your component, you will have to pass it down via a different prop.


So, folks, this is it for this article. Any feedback from your end is welcome.