loading...
Cover image for The significance of React keys - a visual explanation

The significance of React keys - a visual explanation

jtonzing profile image Josh Tonzing Updated on ・4 min read

Disclaimer: This is a gross oversimplification and should be used as a basic guide on how reconciliation works, not a perfect example!

When handling arrays in React, utilisation of the 'key' attribute on each element can be crucial for avoiding needless rerender performance hits. This article will hopefully explain why you should always clearly define your keys, and what you are missing out on if you are not doing so.

Let us start with an array

const joshs = [{  Name: "Josh", }, { Name: "Joshina", }, {  Name: "Notjosh", }]

the business end of our React component which renders said array

<div>
    { joshs.map((person, index) => ( <span key={index}>{person.name}</span>)) }
</div>

and the HTML it outputs

<div>
    <span key=’0’>Josh</span>
    <span key=’1’>Joshina</span>
    <span key=’3’>Notjosh</span>
</div>

Works great!

...but!

A new Josh has arrived, and he's told the others to move out of the way.

 [{ Name: "Mega Josh"}, {  Name: "Josh", }, { Name: "Joshina", }, {  Name: "Notjosh", }]

Our component receives the new list, does its thing...

<div>
    { joshs.map((person, index) => ( <span key={index}>{person.name}</span>)) }
</div>

and prints it out like it did before.

<div>
    <span key=’0’>Mega Josh</span>
    <span key=’1’>Josh</span>
    <span key=’2’>Joshina</span>
    <span key=’3’>Notjosh</span>
</div>

Works great!

...but!

Let us look under the hood on what's actually happening (in a very simplified fashion) with the process that React is going through when it renders its new list.

A React component, when you boil it down to its rawest form (i.e. convert it from JSX), is just an object with a set of properties. These properties define its type, name, state, what props it has received, whether it has children, etc.

Each time a change occurs in our array, a new list of Josh <span> component objects are created. The React reconciler will compare the newly created objects with the current versions it has in the DOM. If any differences are detected between certain properties, it will redraw the components believing that it's the same object, but properties have changed.

So with our example, we have our original array (of components), which can loosely be translated to something like this...

[{
  Type: "span",
  Key: "0",
  Children: "Josh"
}, {
  Type: "span",
  Key: "1",
  Children: "Joshina"
}, {
  Type: "span",
  Key: "2",
  Children: "Notjosh"
}]

The reconciler will look at the key, and the component properties (in our simplified case, the content or children), and then look through its previous list of components to see if it matches any previous combinations.

As our list uses the array index position as its key, when 'Mega Josh' arrives and shifts all the components down one position, every comparison now fails due to React noticing that the keys do not match their previous properties!

[{
  Type: "span",
  Key: "0",                // Expected 0 to match 'Josh'
  Children: "Mega Josh"     // IM DIFFERENT, REDRAW ME
}, {
  Type: "span",
  Key: "1",                // Expected 1 to match 'Joshina'
  Children: "Josh"          // IM DIFFERENT, REDRAW ME
}, {
  Type: "span",
  Key: "2",                // Expected 2 to match 'Notjosh'
  Children: "Joshina"       // IM DIFFERENT, REDRAW ME
}, {
  Type: "span",   
  Key: "3",                // IM NEW
  Children: "Notjosh"       // DRAW ME
}]

But! We can prevent this. If we clearly define a key which is static, unique, and uniquely associated with the properties it is related to, React can acknowledge that it's the same component, even when it has changed its position.

Let us rebuild our components with unique keys

n.b. In this example I use the name attribute to keep our josh objects simple, but this is not best practice as the likelihood of two components having the same key is quite high. Where possible you should always use some sort of primary key from the data object.

<div>
    { people.map((person, index) => ( <span key={`key-${person.name}`}>{person.name}</span>)) }
</div>

the exported HTML will now look like

<div>
    <span key=’key-Josh’>Josh</span>
    <span key=’key-Joshina’>Joshina</span>
    <span key=’key-Notjosh’>Notjosh</span>
</div>

and the updated array HTML

<div>
    <span key='key-Mega Josh'>Josh</span>
    <span key=’key-Josh’>Josh</span>
    <span key=’key-Joshina’>Joshina</span>
    <span key=’key-Notjosh’>Notjosh</span>
</div>

The keys are now unique to their data object (rather than their array position), so when we do our object comparison

[{
  Type: "span",
  Key: "key-Josh",
  Children: "Josh"
}, {
  Type: "span",
  Key: "key-Joshina",
  Children: "Joshina"
}, {
  Type: "span",
  Key: "key-Notjosh",
  Children: "Notjosh"
}]

the reconciler will see that some components haven't changed, they have simply moved, only the new component will be created and inserted at the front of the list, not affecting the components after it. Brilliant!

[{
  Type: "span",
  Key: "key-Mega Josh",    // IM NEW
  Children: "Mega Josh"     // DRAW ME
}, {
  Type: "span",
  Key: "key-Josh",         // Expected 'key-Josh' to match 'Josh'
  Children: "Josh"          // IM THE SAME, DONT REDRAW ME
}, {
  Type: "span",
  Key: "key-Joshina",      // Expected 'key-Joshina' to match 'Joshina'
  Children: "Joshina"       // IM THE SAME, DONT REDRAW ME
}, {
  Type: "span",
  Key: "key-Notjosh",      // Expected 'key-Notjosh' to match 'Notjosh'
  Children: "Notjosh"       // IM THE SAME, DONT REDRAW ME
}]

For some uses, the performance impact may be minimal (or even non-existant if the array never changes order). The benefits with adding keys will be noticeable with very large arrays that get sorted/re-ordered as it will eliminate the need for a majority of your list to be rendered. Magic!

Posted on by:

jtonzing profile

Josh Tonzing

@jtonzing

The personification of imposter syndrome.

Discussion

pic
Editor guide
 

Regarding the opening statement - '...utilisation of the 'key' attribute on each element can be crucial for avoiding needless rerender performance hits.', am I right to say that the 'key' attribute doesn't prevent or reduce the number of times a component is re-rendered? Instead, it's more of making React's life easier when it comes to deciding what change is needed to update the browser DOM?

Love the explanation btw!

 

Typo in 3rd image / Code snippet

<div>
    <span key=’0’>Josh</span>
    <span key=’1’>Joshina</span>
    <span key=’3’>Notjosh</span> (here)
</div>

last span should have key = '2'.

 

Very nice explanation, finally understood :)

 

Finally understood!!! Thanks so much!

 

Great explanation! I now finally understand why keys are needed when rendering component arrays!

 

Finally. Went through a lot of jibberish on React Keys. This article explains it in the most simple and clear way