The Truth About Keys and Re-Renders in React
Let’s explore a concept in React that is often misunderstood: keys. When rendering collections in React, developers frequently use the map function to iterate over items and generate components. However, React requires an additional step: specifying a unique key for each item in the collection. This requirement, often emphasized in React’s documentation (and reinforced by console warnings), is crucial for efficient rendering and maintaining component state across re-renders. To understand why keys are important and often misunderstood, let’s start from the basics.

How React reacts?
Every React component undergoes a series of lifecycle steps. To simplify, these steps can be categorized as: initial rendering (mount), re-rendering (update), and component removal (unmount). For now, we’ll focus on the first two phases. React handles these operations in three distinct steps: the trigger step, the render step, and the commit step.
Trigger step
The trigger step is the starting point for any re-render in React. It occurs whenever an update is necessary, such as when a component’s state changes, a parent component re-renders, or a subscribed context value updates. These actions signal React to check if any part of the component tree needs to be updated, prompting the process to move into the render step.
Render step
The render step marks the next step in React’s component lifecycle. It occurs during the initial mount and whenever updates are triggered (in the trigger step). During this phase, React performs reconciliation, a process driven by a diffing algorithm that utilizes both the Virtual DOM and the React Fiber architecture. Based on the comparison of the previous and updated representations of the component tree, React determines the component’s output, which is essentially the UI. For child components, React recursively invokes their render functions (or component logic in functional components) and those of their descendants to ensure the entire component tree is re-evaluated.
Commit step
Based on the diffing process, React updates only the necessary parts of the real DOM during the commit step. It’s important to note that just because a component’s render function is called, it doesn’t guarantee a DOM update for that component, as rendering and DOM updates are separate processes. Understanding this distinction is crucial for addressing a common myth, which I’ll explain in the next section.
Does keys prevent re-renders?
A common myth surrounding keys is the belief that:
Keys prevent re-renders of unchanged components
Keys play a crucial role in helping React efficiently match elements between renders. While their function extends beyond this, we’ll focus on their role in DOM updates for simplicity. However, it’s important to note that keys alone do not prevent re-renders. React’s default behavior is to re-render all items in a collection whenever the parent component updates, even if the individual items remain unchanged - unless specific optimization techniques are applied.
Let’s explore this with an example involving a List component and an ListItem component. When the List component’s state is updated (e.g. a new item is added), the render phase is triggered. During this phase, React determines what changes need to be applied to the real DOM. It does this by iterating through the List and its child components, invoking their render methods/component’s function execution.
What happens to the existing n elements in the collection after adding a new item? According to the myth, they shouldn’t be re-rendered — but is that really true? Not quite.
Keys role in DOM updates
Keys help us identifies specific elements. The key point here is to follow the specific rules for assigning keys in React to ensure they serve their purpose correctly and efficiently. Keys indeed plays non-directly role in performance optimization but not by avoiding re-renders. Let’s try it on a simple example (also available on the playground)
import React, { useState } from "react";
const App = () => {
return <List />;
};
const List = () => {
const [items, setItems] = useState([
{ id: Math.random() },
{ id: Math.random() },
]);
const addItem = () => {
const newItem = { id: Math.random() };
setItems((currentItems) => [...currentItems, newItem]);
};
return (
<div>
<button onClick={addItem}>Add item</button>
<ul>
{items.map((item) => <ListItem key={item.id} item={item} />)}
</ul>
</div>
);
};
const ListItem = ({ item }) => {
console.log("item render", item.id);
return <li>{item.id}</li>;
};
export default App;
Our setup consists of two components: List, which manages state changes, and ListItem, which represents a single element from the collection. During the initial render, the console output will look something like this:
item render 0.9271951880982141
item render 0.9720048278427091
That seems pretty straightforward, right? Now, let's add a new item by clicking the Add Item button. According to the common belief, we should see just one additional log in the console. To make it clearer, let's first clear the console. Ready? Let’s give it a try:
// 1st (initial) render
item render 0.9271951880982141
item render 0.9720048278427091
// 2nd render (re-render)
item render 0.9271951880982141
item render 0.9720048278427091
item render 0.738685209572542
That’s odd - we see three new logs (five in total if we count the initial two). But surprisingly, this is actually the expected behavior.
So, how does React handle all these changes? As mentioned earlier, every state change triggers a re-render of the component that owns the state, along with all of its child components - in this case, both the List and all its Item components. Notice that even using keys doesn't prevent this.
However, there’s a grain of truth in the idea: keys help React efficiently manage updates to the real DOM. By identifying which items have changed, keys ensure only the new elements are added or updated in the DOM, leaving unchanged elements intact. Let’s try it, this time focusing on DOM updates, not components re-renders. Open dev tools of your browser and find the structure which represents List component. The initial markup looks like this:

Now, let’s add a new item. As you might notice, there's a slight blink that highlights the changes applied to the DOM — but only the newly added item gets highlighted.

I've prepared a playground with an additional MutationObserver to help visualize this behavior more clearly. In the console, you'll see logs showing the re-rendering of all items (each item’s id is printed). However, when it comes to actual DOM mutations, only one mutation is detected — the insertion of the newly added item:
DOM Mutation detected:
Mutation Record {
addedNodes: NodeList [li]
attributeName: null
attributeNamespace: null
nextSibling: null
oldValue: null
previousSibling: li
removedNodes: NodeList []
target: ul
type: "childList"
[[Prototype]]: MutationRecord
}
This distinction between React re-renders and real DOM changes is exactly what this example demonstrates.
Is it possible to modify this default behavior and make only the newly added items re-render? The short answer is yes — there are techniques to achieve that. However, let's save those for another time.
Summary
React keys play a crucial role when working with dynamic lists by helping React accurately identify which items have changed, been added, or removed. This allows React to update the UI correctly during the reconciliation process. However, it's important to understand their true purpose: keys do not prevent re-renders. Instead, they help React maintain consistency and efficiently match elements between renders.
Take a look at our earlier blog post debunking frontend myths:
- To ‘b’ or Not to ‘b’: The Semantic Status of HTML ‘b’ Tag
- Understanding the Hoisting Behavior of let and const
- JavaScript Types De-Objectified
- Eye on ‘i’ — Understanding ‘i’ as a Semantic Element
- Breaking Down the “alt” Attribute Myth in img Tag Best Practices