Matthew C.
—When you render an array of objects, you need to provide a unique key
property for each element rendered for the array item. This tells React which array item each rendered element corresponds to. If you render an array of items as a list, the keys allow React to determine how elements have changed. This is important if the list can be changed by sorting, adding, or deleting. Keys allow React to update the DOM correctly.
React will use the index of the list items as the key if a key is not provided. If the array items can be changed by adding, deleting or re-ordering, the key for each item will not be unique if the key is the array index, which will lead to incorrect DOM updates. A basic example showing this is a list of items without keys, each with a delete button. Suppose you have the following array of food items:
const foodItems = [ { id: "1", value: "milk" }, { id: "2", value: "bread" }, { id: "3", value: "sugar" } ];
These food items are then rendered in an unordered list:
export default function App() { const [items, setItems] = useState(foodItems); function removeItem(item) { setItems(items.filter((i) => i.id !== item.id)); } return ( <div className="keys"> <ul> {items.map((item) => ( <li> <label htmlFor={`${item.id}-input`}>{item.value}</label>{" "} <input id={`${item.id}-input`} defaultValue={item.value} /> <button onClick={() => removeItem(item)}>X</button>{" "} </li> ))} </ul> </div> ); }
When each item in the list is rendered, an input element showing the value of each food item and a delete button for each list item is added. This will render:
Where [milk] is an input with a value of “milk” and [x] is a delete item button. If you delete the second item, the rendered list will be:
There is a mismatch between the list item value and the input value of the second item. Why does this happen? When state changes in a component, React runs a “diffing” algorithm that identifies what has changed in the virtual DOM. It then updates the DOM with the results of diff, which is called reconciliation. When the items
state updates after the second item is deleted, React compares the previous state with the new state to determine how to update the DOM. It only sees the end result of the state change, not how it happened. On the first render, the component will render the following <ul>
element (simplified):
const element = { type: 'ul', key: null, props: { children: [ {type: 'li', key: null, props: {children: 'milk'}}, {type: 'li', key: null, props: {children: 'bread'}}, {type: 'li', key: null, props: {children: 'sugar'}}, ], }, }
After deleting the second item, the following <ul>
element will be rendered:
const element = { type: 'ul', key: null, props: { children: [ {type: 'li', key: null, props: {children: 'milk'}}, {type: 'li', key: null, props: {children: 'sugar'}}, ], }, }
React does not know how the new element state was reached when it does a diff to see what has changed. It does not know where the item was removed from.
What React does is assume that the index is the key if key
is not provided. So it assumes that the third item was removed and the second item was re-named. This would not be a problem if the <li>
elements had no components that have state. In this example the <input>
values of each <li>
item maintain state. So the second item’s <input>
maintains its original state, and it does not match the list item name any more:
This also shows that using an index for a key may remove the following warning in the console:
Warning: Each child in a list should have a unique "key" prop.
However, it does not fix the problem.
This issue also occurs if the list items are re-arranged or if items are added.
In this simple example, the issue is visual. If the list renders components that have side effects such as data fetching, it can cause all sorts of other issues.
Add a unique key
prop to each list item. This gives React more information, which helps it update the UI correctly. Keys must be unique. In the example code used, the food item id
would make a good key.
On first render the component will render the following React <ul>
element (simplified) if the food item id
is used as the key:
const element = { type: 'ul', key: null, props: { children: [ {type: 'li', key: 1, props: {children: 'milk'}}, {type: 'li', key: 2, props: {children: 'bread'}}, {type: 'li', key: 3, props: {children: 'sugar'}}, ], }, }
After deleting the second item, the following <ul>
element will be rendered:
const element = { type: 'ul', key: null, props: { children: [ {type: 'li', key: 1, props: {children: 'milk'}}, {type: 'li', key: 3, props: {children: 'sugar'}}, ], }, }
Now when React does its diff to see what has changed, it can tell that the second item was removed and update the DOM correctly.
Tasty treats for web developers brought to you by Sentry. Get tips and tricks from Wes Bos and Scott Tolinski.
SEE EPISODESConsidered “not bad” by 4 million developers and more than 100,000 organizations worldwide, Sentry provides code-level observability to many of the world’s best-known companies like Disney, Peloton, Cloudflare, Eventbrite, Slack, Supercell, and Rockstar Games. Each month we process billions of exceptions from the most popular products on the internet.
Here’s a quick look at how Sentry handles your personal information (PII).
×We collect PII about people browsing our website, users of the Sentry service, prospective customers, and people who otherwise interact with us.
What if my PII is included in data sent to Sentry by a Sentry customer (e.g., someone using Sentry to monitor their app)? In this case you have to contact the Sentry customer (e.g., the maker of the app). We do not control the data that is sent to us through the Sentry service for the purposes of application monitoring.
Am I included?We may disclose your PII to the following type of recipients:
You may have the following rights related to your PII:
If you have any questions or concerns about your privacy at Sentry, please email us at compliance@sentry.io.
If you are a California resident, see our Supplemental notice.