Significance of keys in rendering lists in React
A demonstration to show that when array index is used as the key attribute while rendering lists in React, it can cause bugs and performance degradation.
Why is using array indices as keys while rendering a list an anti-pattern in React?
- React uses keys to identify/differentiate between items.
- Array indices do not prove to be stable keys and using them while looping through items while rendering, can cause bugs and performance issues, because of the way React is designed. This is evident from the examples discussed below.
- This is an anti-pattern only in cases where the order of the elements of the list may change. If the order is guaranteed to always remain the same, and the items getting the keys do not have state associated with them, then it is okay to use the array indices as keys.
A demonstration of the bug array index keys can cause
- Say we enter values in the textboxes, for example “red” in the first textbox, “blue” in the second, and “green” in the third.
- Now remove the first textbox by clicking on the “Remove first item” button.
- We would expect that the textbox with label and content “red” should be removed from the UI, leaving out textboxes labelled “blue” and “green”. But what is rendered is textboxes labelled “blue” and “green”, with values “red” and “blue”, respectively. Major mess up!
- Note that this bug is evident only when the list items have state associated with them. For use cases that don’t have state involved with the list items, like the example — “Rendering static objects (without state) inefficiently with array index as keys”, the use of the array index as key does not cause this bug, but definitely causes a performance drop.
Analyzing the cause of the bug
- It is the way React’s reconciliation algorithm is designed that causes this bug to surface.
- Let’s take a moment to understand the code for the example. See full example here.
- After the initial render, when we enter the data into the 3 textboxes (let’s say “red”, “blue”, and “green”) these textboxes are rendered on the screen with keys 0, 1, and 2 respectively.
- The current state of the app visualized in tabular form:
Unstable key — — — —Textbox content
0 — — — — —red
1 — — — — — blue
2 — — — — — green
- When we delete the first item, i.e., the “red” textbox, this is what the new state becomes with using the array indices as keys:
Unstable key — — — — Textbox content
0 — — — — — -blue
1 — — — — — -green
- We can see that the state is updated correctly as we expected. The bug is because of how this new state is rendered on the UI.
- With these 2 states in hand between renders, it is the job of React’s reconciliation mechanism to run its diffing algorithm, to figure out which elements need to be removed/updated from the real DOM. It doesn’t care what the content of the DOM nodes (i.e., these textboxes) is between renders. Given that these DOM nodes have the same type (i.e., <input type=”text”), it only identifies/differentiates between these DOM nodes from the key attribute that is on these DOM nodes.
- So here is what it thinks:
“The item with key 0 is still present in the second render, so it must not have changed, so I can let that DOM node be — So the textbox with key 0 from the initial render stays.” But it gets the state “red” from the previous render and the label “blue” from the current render 😧
“Similarly, the item with key 1 is also still present in the second render, so it must also not have changed, so I can let that DOM node too be.” So the textbox with key 1 from the initial render stays, but with state as “blue” and label as “green”😧
“The item with key 2 was there in the first render but is missing in the second render, so that item should be deleted from the DOM.” And so it deletes the textbox with key 2 from the DOM 😧
While in reality we intended to delete the first, i.e., textbox with key 0.
The Fix: Use stable keys
- After the initial render, when we again enter the same data into the 3 textboxes (let’s say “red”, “blue”, and “green”) these textboxes are now rendered on the screen with keys “red”, “blue”, and “green” respectively.
- The current state of the app visualized in tabular form:
Stable key — — — — Textbox content
red— — — — — red
blue— — — — — blue
green — — — — — green
- When we delete the first item, i.e., the “red” textbox, this is what the new state becomes with using the array indices as keys:
Stable key — — — — Textbox content
blue— — — — — -blue
green — — — — — -green
Now the reconciliation mechanism clearly understands that the DOM node with key “red” is missing in the second render, and all the other items are still the same (because their keys have not changed) so the “red” node needs to be removed from the real DOM, and “blue” and “green” nodes need to remain. This helps to correctly maintain the state between renders 😃
Analyzing the cause of the performance drop
As mentioned before the above bug surfaces only when the list items are given unstable keys like array indices, and these items have a state associated with them.
When the list items don’t have a state involved (let’s call them static items), there is no such bug but definitely performance issues.
Let’s examine how:
State object after initial render:
Unstable key— — —Static item
0 — — — — red
1 — — — — blue
2 — — — — green
State object after first item is removed:
Unstable key — — — —Static item
0 — — — — — -blue
1 — — — — — -green
Again, the update to the state object is correct but the way the real DOM is updated by the reconciliation mechanism is slow. When React reconciles and decides the operations to be made to update the real DOM, it thinks that:
Item with key 0 has changed from “red” to “blue”. Apply this operation to the real DOM.
Item with key 1 has changed from “blue” to “green”. Apply this operation to the real DOM.
Item with key 2 has been deleted. Apply this operation to the real DOM.
So it ends up applying 3 operations to the real DOM, while it could have optimally managed with just 1 operation as we can see below.
Performance improvement when stable keys are used
On the other hand, had stable keys been given to these items, there would have been only one DOM operation for React to perform, that is to delete the “red” item, as outlined below:
State object after initial render:
Stable key — — — Static item
red — — — — — — red
blue — — — — — --blue
green — — — —-- green
State object after first item is removed:
Stable key — — — Static item
blue — — — — — — blue
green — — — — — green
Now React’s reconciliation clearly understands that the DOM node with key “red” is missing in the second render, and all the other items are still the same (because their keys have not changed) so the “red” node needs to be removed from the real DOM, and “blue” and “green” nodes need to remain.
This results in only 1 operation on the real DOM.
Operations on the real DOM are expensive and React is designed to optimally minimise the number of such operations it does.
Parting thoughts
The examples discussed in this post are toy examples. But let’s think of what would happen if array indices were used as keys in an enterprise application, state getting messed up mysteriously or simple operations like deleting an item taking annoyingly too long. These are hard to catch bugs.
I hope that this blog was helpful. I would love to hear your feedback in the comments.
Happy coding ❤️
Full working example
https://stackblitz.com/edit/stackblitz-starters-cfgwsr
References
https://legacy.reactjs.org/docs/reconciliation.html
https://react.dev/learn/rendering-lists#why-does-react-need-keys