Help
Support Us

Keys

In chapter one, we saw how Preact uses a Virtual DOM to calculate what changed between two trees described by our JSX, then applies those changes to the HTML DOM to update pages. This works well for most scenarios, but occasionally requires that Preact "guesses" how the shape of the tree has changed between two renders.

The most common scenario where Preact's guess is likely to be different than our intent is when comparing lists. Consider a simple to-do list component:

export default function TodoList() {
  const [todos, setTodos] = useState(['wake up', 'make bed'])

  function wakeUp() {
    setTodos(['make bed'])
  }

  return (
    <div>
      <ul>
        {todos.map(todo => (
          <li>{todo}</li>
        ))}
      </ul>
      <button onClick={wakeUp}>I'm Awake!</button>
    </div>
  )
}

The first time this component is rendered, two <li> list items will be drawn. After clicking the "I'm Awake!" button, our todos state Array is updated to contain only the second item, "make bed".

Here's what Preact "sees" for the first and second renders:

First Render Second Render
<div>
  <ul>
    <li>wake up</li>
    <li>make bed</li>
  </ul>
  <button>I'm Awake!</button>
</div>
<div>
  <ul>
    <li>make bed</li>

  </ul>
  <button>I'm Awake!</button>
</div>

Notice a problem? While it's clear to us that the first list item ("wake up") was removed, Preact doesn't know that. All Preact sees is that there were two items, and now there is one. When applying this update, it will actually remove the second item (<li>make bed</li>), then update the text of the first item from wake up to make bed.

The result is technically correct – a single item with the text "make bed" – the way we arrived at that result was suboptimal. Imagine if there were 1000 list items and we removed the first item: instead of removing a single <li>, Preact would update the text of the first 999 other items and remove the last one.

The key to list rendering

In situations like the previous example, items are changing order. We need a way to help Preact know which items are which, so it can detect when each item is added, removed or replaced. To do this, we can add a key prop to each item.

The key prop is an identifier for a given element. Instead of comparing the order of elements between two trees, elements with a key prop are compared by finding the previous element with that same key prop value. A key can be any type of value, as long as it is "stable" between renders: repeated renders of the same item should have the exact same key prop value.

Let's add keys to the previous example. Since our todo list is a simple Array of strings that don't change, we can use those strings as keys:

export default function TodoList() {
  const [todos, setTodos] = useState(['wake up', 'make bed'])

  function wakeUp() {
    setTodos(['make bed'])
  }

  return (
    <div>
      <ul>
        {todos.map(todo => (
          <li key={todo}>{todo}</li>
          //  ^^^^^^^^^^ adding a key prop
        ))}
      </ul>
      <button onClick={wakeUp}>I'm Awake!</button>
    </div>
  )
}

The first time we render this new version of the <TodoList> component, two <li> items will be drawn. When clicking the "I'm Awake!" button, our todos state Array is updated to contain only the second item, "make bed".

Here's what Preact sees now that we've added key to the list items:

First Render Second Render
<div>
  <ul>
    <li key="wake up">wake up</li>
    <li key="make bed">make bed</li>
  </ul>
  <button>I'm Awake!</button>
</div>
<div>
  <ul>

    <li key="make bed">make bed</li>
  </ul>
  <button>I'm Awake!</button>
</div>

This time, Preact can see that the first item was removed, because the second tree is missing an item with key="wake up". It will remove the first item, and leave the second item untouched.

When not to use keys

One of the most common pitfalls developers encounter with keys is accidentally choosing keys that are unstable between renders. In our example, imagine if we had used the index argument from map() as our key value rather than the item string itself:

items.map((item, index) => <li key={index}>{item}</li>

This would result in Preact seeing the following trees on the first and second render:

First Render Second Render
<div>
  <ul>
    <li key={0}>wake up</li>
    <li key={1}>make bed</li>
  </ul>
  <button>I'm Awake!</button>
</div>
<div>
  <ul>

    <li key={0}>make bed</li>
  </ul>
  <button>I'm Awake!</button>
</div>

The problem is that index doesn't actually identify a value in our list, it identifies a position. Rendering this way actually forces Preact to match items in-order, which is what it would have done if no keys were present. Using index keys can even force expensive or broken output when applied to list items with differing type, since keys cannot match elements with differing type.

πŸš™ Analogy Time! Imagine you leave your car with a valet car park.

When you return to pick up your car, you tell the valet you drive a grey SUV. Unfortunately, over half of the parked cars are grey SUV's, and you end up with someone else's car. The next grey SUV owner gets the wrong one, and so on.

If you instead tell the valet you drive a grey SUV with the license plate "PR3ACT", you can be sure that your own car will be returned.

As a general rule of thumb, never use an Array or loop index as a key. Use the list item value itself, or generate a unique ID for items and use that:

const todos = [
  { id: 1, text: 'wake up' },
  { id: 2, text: 'make bed' }
]

export default function ToDos() {
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          {todo.text}
        </li>
      ))}
    </ul>
  )
}

Remember: if you genuinely can't find a stable key, it's better to omit the key prop entirely than to use an index as a key.

Try it!

For this chapter's exercise, we'll combine what we learned about keys with our knowledge of side effects from the previous chapter.

Use an effect to call the provided getTodos() function after <TodoList> is first rendered. Note that this function returns a Promise, which you can obtain the value of by calling .then(value => { }). Once you have the Promise's value, store it in the todos useState hook by calling its associated setTodos method.

Finally, update the JSX to render each item from todos as an <li> containing that todo item's .text property value.

Loading...