React Keys

Chapter 18 12 mins

Learning outcomes:

  1. Reviewing diffing and reconciliation
  2. What are keys and the key prop
  3. How React uses keys internally
  4. A problem solved by keys

Reviewing diffing and reconciliation

When React renders the virtual DOM tree, except for the very first time doing so, it compares the new tree with previous tree to determine what has changed and what not.

Based on this, it evaluates which new DOM nodes to create, and which nodes to keep in tact with just their properties and/or content being modified.

In this comparison process, keys belonging to React elements play a vital role in the utlimate outcome of whether to dump a DOM node and create a new one or to reuse that same node and just modify its properties.

Let's see what this means.

Imagine that we have the following virtual DOM tree, inside the #root element. It consists of elements representing component instances as well as DOM elements:

<App>
   <h1 className="heading">A heading<h1>
   <p>A paragraph<p>
</App>

Now suppose that we trigger a state change that leads to the following tree:

<App>
   <h1 className="heading">A new heading<h1>
   <p>A new paragraph<p>
</App>

The most important thing to notice here is that the structure of the tree hasn't changed, and neither the type of elements in any given position. It's only the content that has changed.

What React will do is that it'll diff both of these trees and make the following conclusion to only change the content of the <h1> and the <p> elements; the element nodes themselves remain in tact.

Now, let's suppose we have the following inputs of the diffing algorithm:

<App>
   <h1 className="heading">A heading<h1>
   <p>A paragraph<p>
</App>
<App>
   <p className="heading">A heading<p>
   <p>A paragraph<p>
</App>

This time, when we previously had an <h1> element, we now have a <p> element, albeit with the same properties and content. The structure of the tree is still the same, but now the type of one element has changed.

In this case, the differ will determine that it needs to throw away the <h1> element node and put a <p> element node in place of it.

This is diffing and reconciliation action.

One of the key players in diffing is the key prop. Let's see what exactly is it.

What are keys in React?

In React, keys are associated with elements.

A key is a unique identifier for an element amongst its siblings.

The sole purpose of keys is to improve the efficiency and predictability of the reconciliation algorithm where possible.

Typically, the usage of keys is desirable only when dealing with arrays of elements.

If each item in the array is given a key before being rendered, it'll make sure that on subsequent updates to the array, the least number of DOM mutations occur. We'll see what this means later on in this chapter.

However, sometimes, we might want to manually add keys to given React elements as well.

To set the key of an element, we provide it a key prop with a value that we feel can be used uniquely.

We can even generate unique IDs using JavaScript APIs, such as Crypto, or third-party libraries, such as uuid, if we want to be 'actually' unique. We won't be considering this approach for now.

How React uses keys internally?

It's not really difficult to think about the internal workings of keys in React, once we experiment around with a few diffing problems.

Essentially, keys apply at the children level of a given element.

As stated in the docs, keys in React could be thought of as filenames — the names help us rightaway select the file we need. Without the name, we'd only be able to rely on the order of the files, which can surely be modified, in which case we won't be sure about which file to select.

With keys, React can easily tell which elements already existed inside a given element, and which elements are new additions. And then it only mutates the DOM to add the missing elements.

While using keys, it's not mandatory for elements having those keys to be in the same positions as before to prevent getting DOM mutations triggered. What only matters is that amongst the siblings of the elements that have keys, there exist elements with keys seen before.

If that's the case, React only mutates the DOM to add element nodes for React elements that have new keys or mutates the DOM to remove element nodes for those React elements that just don't exist now in the virtual DOM tree.

The elements with keys seen before as well are simply not touched upon.

As you would agree, this means two things:

  1. First, that using keys could definitely make React efficient in its reconciliation.
  2. Second, it's NOT useful to use indexes of the elements of an array as keys of those elements.

Using the indexes of array elements as keys is effectively equivalent to not using any keys at all!

Without keys, as we learnt above, React relies on the ordering of elements to diff them. However, with keys that are based on array indexes, this remains the case — the indexes are based on the ordering of the elements, and so is the diffing.

Whenever possible, it's strongly recommended to use unique keys for arrays of elements in React, that aren't obtained via the indexes of those elements!

A problem solved by keys

Suppose we have the following array containing the names of some popular programming languages:

const languages = ['Java', 'Perl', 'Scala', 'Ruby', 'C++'];

And now suppose that we want to render each item of this array as an <Item> element, using a component Item, part of a parent component <List>. Fairly simple.

The <List> element will render an <ol> element while <Item> will render an <li> element.

const languages = ['Java', 'Perl', 'Scala', 'Ruby', 'C++'];

function Item({children}) {
   return (
      <li>{children}</li>
   );
}

function List() {
   return (
      <ol>
         {languages.map(language => (
            <Item>{language}</Item>
         ))}
      </ol>
   );
}

function App() {
   return <List/>;
}

Clearly this was very easy.

Live Example

Now, let's add some ref data to each <Item> and then see the issue caused thereafter.

Each Item component will have a ref called seen, which will specify if the given item has already been rendered in the list. Based on the value of seen, we'll render a small <div> element to act kind of as a 'seen' tag next to each item.

Here's the redefined Item component:

function Item({children}) {
   const seen = useRef(false);

   // After the render, this item is 'seen'.
   useEffect(() => {
      seen.current = true;
   }, []);

   return (
      <li>
         {children}
         {seen && <span className="seen">Seen</span>}
      </li>
   );
}

Now, let's rerun the code:

Live Example

The output is the exact same as before. Well, it should indeed be. It's only once we add a new programming language to the array that we'll witness the change.

So, let's go on and modify our example.

We'll make the provided items prop a state of the List component. Also, we'll add a button to update this items state by adding a new language right at its beginning.

Here's the new code:

function List() {
   const [items, setItems] = useState(languages);

   return (
      <>
         <ol>
            {items.map(item => (
               <Item>{item}</Item>
            ))}
         </ol>
         <button onClick={() => setItems(['Ada', ...items])}>Add new language 'Ada'</button>
      </>
   );
}

Now, let's try the example and see the result.

Live Example

Contrary to our expectation, when we click the button and get the new programming language 'Ada' to be added, 'Ada' gets a 'seen' tag, which shouldn't be the case. This new item 'Ada' wasn't already there before in the list, and so it shouldn't be 'seen'. Moreoever, the last language, i.e. 'C++', seems to be 'unseen' which is again not what should be the case.

So what's the issue? Well, it's the diffing algorithm.

Here's the resulting JSX after the click of the button:

<ol>
   <li>Ada</li>
   <li>Java</li>
   <li>Perl</li>
   <li>Scala</li>
   <li>Ruby</li>
   <li>C++</li>
</ol>

And here's the previous one:

<ol>
   <li>Java</li>
   <li>Perl</li>
   <li>Scala</li>
   <li>Ruby</li>
   <li>C++</li>
</ol>

While diffing both these trees of elements, React realizes that the element's type is the same across all four starting <Item>s — it's only their content that has changed. And it also realizes that the last <Item> is a new addition. Likewise, in the reconciliation, the content of the first four <li>s is changed, while a new fifth <li> is added.

And since the first four <Item>s remain in tact, their refs also remains in tact. This is what causes the problem because in the new render, the content of the first <Item> is 'Ada', yet its seen ref is the same as before, technically belonging to the language 'Java'.

The solution to this problem is extremely simple — use key.

Here's how it'll work. We apply a key to each and every <Item> element. This key is the name of the language since it's most probably gonna be unique in this example. Using the index of each array element isn't gonna work — it'll produce exactly the same outcome as before.

Let's do this and then see the result:

function List() {
   const [items, setItems] = useState(languages);

   return (
      <>
         <ol>
            {items.map(item => (
               <Item key={item}>{item}</Item>
            ))}
         </ol>
         <button onClick={() => setItems(['Ada', ...items])}>Add new language 'Ada'</button>
      </>
   );
}

Alright, let's try interacting with the example now.

Live Example

Voila! It works just as we wanted it to.

Let's review what happens in the diffing this time.

Here's the JSX after the click of the button.

<ol>
   <li key='Ada'>Ada</li>
   <li key='Java'>Java</li>
   <li key='Perl'>Perl</li>
   <li key='Scala'>Scala</li>
   <li key='Ruby'>Ruby</li>
   <li key='C++'>C++</li>
</ol>

And here's the one before it:

<ol>
   <li key='Java'>Java</li>
   <li key='Perl'>Perl</li>
   <li key='Scala'>Scala</li>
   <li key='Ruby'>Ruby</li>
   <li key='C++'>C++</li>
</ol>

While comparing both of these trees of elements, React realizes that the type of the first <Item> element in the new tree is the same as that of the first <Item> element in the previous tree. However, since the key is different (now it's 'Ada'), it realizes that this <Item> element ought to be created as a new element.

The rest of the five <Item>s in the new tree have keys on them that already exist in the previous tree, and so diffing says that they should be kept in tact. Ultimately, in the reconciliation, only one new <li> element is added right at the start of the <ol> element.

Simple?

Using key, React no longer needs to rely just on the order of elements to determine if they must be changed or not — it can rely on the value of key as well to make this decision.

"I created Codeguage to save you from falling into the same learning conundrums that I fell into."

— Bilal Adnan, Founder of Codeguage