Code Review: Use A Good Key When Rendering Lists In Your React
Rendering a list of items is a common scenario for most UI these days. In its official document, React states key
as an essential property for your list. In this post, I’ll summarize some notes on its usage.
Index Is Good Enough For Some Cases
Static Lists
Suppose you only display items without any chance to reorder the list, such as deleting an object, filtering, or moving things around. In that case, index
is a safe and easy way for key
.
Lists of Stateless Components
If your items are stateless components, it’s also OK to use index
as key
even though you have to change their orders.
Although this method is not optimized for render performance, it doesn’t impact directly to user experience. If your list is small and the child components are not so complex, you can use index
as key
. (You can read more about how React uses key
to optimize the rendering process in its official document).
In the sample below, I have a list of items with different labels and colors. I stored that info as a state of the list and used stateless components for rendering its children. When I delete some items, the labels and colors are still matching well. It means index
works well as a key
in this case.
import { useState } from 'react';
import { getColor } from './utils';
export const ListOfStatelessComponents = () => {
const [values, setValues] = useState([
{
name: 'Item 1',
colorIndex: 0,
},
{
name: 'Item 2',
colorIndex: 0,
},
{
name: 'Item 3',
colorIndex: 0,
},
{
name: 'Item 4',
colorIndex: 0,
},
{
name: 'Item 5',
colorIndex: 0,
},
]);
const del = (index) => {
const newValues = [...values];
newValues.splice(index, 1);
setValues(newValues);
};
const countClick = (index) => {
const newValues = [...values];
newValues[index].colorIndex++;
setValues(newValues);
};
return (
<>
{values.map((value, index) => (
<p
key={index}
style={{ backgroundColor: `${getColor(value.colorIndex)}` }}
>
<span onClick={() => countClick(index)}>{value.name}</span>
<button onClick={() => del(index)}>Delete</button>
</p>
))}
</>
);
};
When To Avoid Using Index
If your items are stateful components and users can change their orders, using index
as key
will mess up states of items.
The sample below has the same functionality as the above one. The difference is I moved the color code into state
of child components. It means I’m now using stateful components instead of stateless. When I delete some items, the colors are all messed up.
import { useState } from 'react';
import { getColor } from './utils';
const ChildItem = ({ value, onDelete, ...rest }) => {
const [colorIndex, setColorIndex] = useState(0);
return (
<p style={{ backgroundColor: `${getColor(colorIndex)}` }} {...rest}>
<span onClick={() => setColorIndex(colorIndex + 1)}>{value}</span>
<button onClick={onDelete}>Delete</button>
</p>
);
};
export const ListOfStatefulComponents = () => {
const [values, setValues] = useState([
'Item 1',
'Item 2',
'Item 3',
'Item 4',
'Item 5',
]);
const del = (index) => {
const newValues = [...values];
newValues.splice(index, 1);
setValues(newValues);
};
return (
<>
{values.map((value, index) => (
<ChildItem key={index} value={value} onDelete={() => del(index)} />
))}
</>
);
};
How To Deal With It
When you render a list of stateful components and want to re-order them, your data must have a unique value to use as index
. If it hasn’t, you should generate one.
In the sample below, my list is an array of strings that don’t have any unique property. I must convert each item into an object with a newly generated ID, which became key
for my stateful child components. This ID ensures correct states when I re-order the list.
import { useState } from 'react';
import { getColor } from './utils';
const ChildItem = ({ value, onDelete, ...rest }) => {
const [colorIndex, setColorIndex] = useState(0);
return (
<p style={{ backgroundColor: `${getColor(colorIndex)}` }} {...rest}>
<span onClick={() => setColorIndex(colorIndex + 1)}>{value}</span>
<button onClick={onDelete}>Delete</button>
</p>
);
};
const generateId = (() => {
let counter = 1;
return () => `id_${++counter}`;
})();
export const ListOfStatefulComponentsWithCorrectKeys = () => {
const [values, setValues] = useState(
['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'].map((value) => {
return { value, id: generateId() };
})
);
const del = (index) => {
const newValues = [...values];
newValues.splice(index, 1);
setValues(newValues);
};
return (
<>
{values.map((item, index) => (
<ChildItem
key={item.id}
value={item.value}
onDelete={() => del(index)}
/>
))}
</>
);
};
Don’t Generate ID Within Render
Don’t generate ID within render. This action is a bad practice for many reasons:
- The keys will change every render. It forces React to see the items as newly added and always re-render.
- Generate ID could be an expensive task for rendering action.
The ID should be a permanent value for your list. It should be generated at the caller (if you use props) or go along with your state.
// DON'T: generate key when render
const MyList = ({ items }) => {
return (
<>
{items.map(item => (
<p key={generateId()}>
{item.value}
</p>
))}
</>
);
}
// DO: generate ID at caller level if the list was passed by props
const Parent = () => {
const renderingItems = values.map(value => {
return {
id: generateId(),
value,
}
})
return (
...
<MyList items={renderingItems} />
...
)
}
const MyList = ({ items }) => {
return (
<>
{items.map(item => (
<p key={item.key}>
{item.value}
</p>
))}
</>
);
}
// DO: If the list stored in a state, generate ID when the item was added in
const MyList = () => {
const [renderingItems, setRenderingItems] = useState(values.map(value => {
return {
id: generateId(),
value,
}
}));
return (
<>
{renderingItems.map(item => (
<p key={item.key}>
{item.value}
</p>
))}
</>
);
}
Conclusion
- If your data has a unique property, use it.
- If you don’t need to re-order your list, using
index
is good enough. - If you need to re-order your list and your child components are stateless, it’s okay to use
index
although not optimized for render performance. - In other cases, you have to generate a unique value for your list data first. This value should be permanent and shouldn’t change in renders.
=====
This article is part of my series about Code Review, which talks about my notes while reviewing code. Each of them could be tiny or nothing special for some people. But because a slight mistake can cost developers a lot to figure out, it could be helpful for others to mind all the possibilities.
I’ve put the samples above in a Github repository in case anyone wants to try them out.