React lists and the key prop warning

React lists and the key prop warning

Why does react need a key prop while rendering a list of JSX elements?

If you have been using react for some time, then you must have rendered a list of elements (probably) using a map(). And while doing so, you might have encountered the typical Each child in a list should have a unique "key" prop. warning.

list key warning.JPG

Often beginners in React, tend to get rid of the warning by using the index of the element in the array as the key, but if the order or content of list items is to change this becomes an anti-pattern.

We don’t recommend using indexes for keys if the order of items may change. This can negatively impact performance and may cause issues with component state. If you choose not to assign an explicit key to list items then React will default to using indexes as keys. - React Docs

In this blog we'll discuss why does react need key prop while rendering elements from a list, but before we do that let's have a quick look into how react renders elements from JSX. Consider the below snippet.

const persons= [
  {id: 'MS', name: 'Michael'},
  {id: 'CB', name: 'Creed'},
  {id: 'DS', name: 'Dwight'},
  {id: 'JH', name: 'Jim'},
]

 const App = () => {
    return (
       <h1>persons[0].name</h1>
  )
}

The functional components in React return a plain JS object, which is later rendered to DOM by ReactDOM. The JS object returned by the App component looks something like this:

 {
     type: "h1",
     key: null,
     ref: null,
     props: { children : "Michael" },
     _owner: null,
     _store: {}
}

When this object is passed to ReactDOM.render, the ReactDOM API interprets this object and creates a DOM node with the required properties.

Now let's render a list without the key prop, of course you'll get a warning but we'll see how react understands and works with lists, and why it gets mad when it does not see the key prop.

const persons= [
  {id: 'MS', name: 'Michael'},
  {id: 'CB', name: 'Creed'},
  {id: 'DS', name: 'Dwight'},
  {id: 'JH', name: 'Jim'},
]

 const App = () => {
    return (
       <ul>
          {persons.map((person) => (
            <li>{person.name}</li>
          ))}
    </ul>
  )
}

React does not have access to the function implementation, and it doesn't care, all it needs is a function or API that it can pass arguments to and receive a returned JSX element from, which can then be painted to the DOM.

So when React runs the App function above, it gets the following object returned from the function, and renders it to the DOM, of course with that key warning in the console.

const element = {
  type: 'ul',
  key: null,
  props: {
    children: [
      {type: 'li', key: null, props: {children: 'Michael'}},
      {type: 'li', key: null, props: {children: 'Creed'}},
      {type: 'li', key: null, props: {children: 'Dwight'}},
      {type: 'li', key: null, props: {children: 'Jim'}},
    ],
  },
}

Now if a state updater function runs, React needs to re-render and calls the App function again, and receives the below returned object.

const element = {
  type: 'ul',
  key: null,
  props: {
    children: [
      {type: 'li', key: null, props: {children: 'Michael'}},
      {type: 'li', key: null, props: {children: 'Dwight'}},
      {type: 'li', key: null, props: {children: 'Jim'}},
    ],
  },
}

React has no way of knowing how this final state was reached, but is concerned to paint the DOM with this state. It doesn't know if list item 'Creed' was deleted, or whether it was renamed to 'Dwight', 'Dwight' renamed to 'Jim', and 'Jim' was deleted. There are multiple ways that the final state could be reached from the initial state. But as with any computer program, there can be no ambiguity with React, so it has a defined procedure of how to deal with these state changes, in case of no explicit key prop.

React will put in the index of the list item, as the key and start comparing the list items from the previous render to those in the final state corresponding to the index as key. What this means is it will look at list item with key 0, and observe no difference, will move on to the next item with key 1 and encounter that the children has been changed from 'Creed' to 'Dwight'. So it will mutate each child where there is a difference instead of realizing that it could just remove the item 'Creed' right away and keep the rest items ('Dwight','Jim') as they are, if that was intended in the state update.

This unnecessary mounting and unmounting of components triggers effects, and is not optimal. This is where the explicit key props help. According to the react docs,

When children have keys, React uses the key to match children in the original tree with children in the subsequent tree

In our example, if we modify the code to add key props to list items, we can optimize and remove errors with rendering, like so.

const persons= [
  {id: 'MS', name: 'Michael'},
  {id: 'CB', name: 'Creed'},
  {id: 'DS', name: 'Dwight'},
  {id: 'JH', name: 'Jim'},
]

 const App = () => {
    return (
       <ul>
          {persons.map((person) => (
            <li key={person.id}>{person.name}</li>
          ))}
    </ul>
  )
}

Now when react compares the below two objects using the key prop:

// Initial object
const element = {
  type: 'ul',
  key: null,
  props: {
    children: [
      {type: 'li', key:'MS', props: {children: 'Michael'}},
      {type: 'li', key: 'CB', props: {children: 'Creed'}},
      {type: 'li', key: 'DS', props: {children: 'Dwight'}},
      {type: 'li', key: 'JH', props: {children: 'Jim'}},
    ],
  },
}

//Final Object

const element = {
  type: 'ul',
  key: null,
  props: {
    children: [
      {type: 'li', key:'MS', props: {children: 'Michael'}},
      {type: 'li', key: 'DS', props: {children: 'Dwight'}},
      {type: 'li', key: 'JH', props: {children: 'Jim'}},
    ],
  },
}

React knows exactly what to do, it notices that element with key 'CB' is gone, so it removes it from the page, and observes that there is no change with elements with other keys, so it renders them as is. This is much more nuanced and clear than the previous comparison using index as keys.

Conclusion

The key prop for items in the same list should be unique, however items in different lists can have overlapping keys. This key is usually supplied by database id for the elements being rendered.

Hopefully this article helps you get a bit clarity on why react needs a key prop when working with lists, why is using the index of the list item is a bad idea, and how React actually uses the key prop to track the elements to update them if needed on state change.