列表渲染与 key
几乎任何真实应用都有"列表"——待办事项、商品、用户、菜单。React 没有 v-for 这种专用指令,而是直接用 JS 的 map 把数组变成一组 JSX。这听起来简单,但有一个关键细节 key,处理不好会引发诡异 bug,这一章我们彻底讲清。
1. 用 .map() 渲染列表
React 渲染列表的核心套路:数组.map(项 => JSX),map 返回一个 JSX 数组,React 自动展开渲染。
function NumberList({ numbers }) {
// 用 .map() 把数组转成一组 JSX 元素
return (
<ul>
{numbers.map(num => (
<li key={num}>{num}</li>
))}
</ul>
);
}
function App() {
return <NumberList numbers={[1, 2, 3, 4, 5]} />;
}
// 渲染:
// · 1
// · 2
// · 3
// · 4
// · 5每个返回的 JSX 元素必须带一个 key 属性——这是 React 的硬性要求(不写控制台会报红色警告)。下面专门讲 key 为什么重要。
2. key 是什么?为什么必须有?
key 是 React 用来识别列表中每个元素的"身份"的特殊属性。当列表数据变化(增、删、排序)时,React 用 key 判断"哪些元素是新的、哪些只是位置变了、哪些被删了",从而只更新变化的部分,而不是整个列表重新渲染。
- 没有 key:React 默认按顺序对比,列表中间插入一项时,后面所有项都要更新,性能差。
- 有稳定 key:React 知道"这项是新的(key 没见过)",只创建它,其他原样保留。
3. 为什么不要用 index 当 key(经典陷阱)
新手最容易犯的错:用数组下标 index 当 key。看起来"唯一"就行,但当列表顺序变化时会出大问题:
// ❌ 错误:用数组下标 index 当 key
{todos.map((todo, index) => (
<li key={index}>
<input type="checkbox" checked={todo.done} />
{todo.text}
</li>
))}
// ✅ 正确:用数据本身的稳定 id 当 key
{todos.map(todo => (
<li key={todo.id}>
<input type="checkbox" checked={todo.done} />
{todo.text}
</li>
))}想象一个场景:待办列表 [A, B, C],你在开头插入一项 X,变成 [X, A, B, C]。
- 用 index 当 key:X 的 key 是 0,但原来 key=0 的是 A。React 以为"A 变成了 X",于是复用了 A 那个 DOM 节点,把内容改成 X——但 A 里那个
input的勾选状态(是组件内部 state)还在,导致勾选错位。 - 用 id 当 key:X 的 key 是它自己的 id,React 知道这是新元素,创建新 DOM,A、B、C 原样保留——状态正确。
所以规则:key 必须稳定(数据不变就不变)、唯一(同级不重复)。优先用数据的 id,绝不要用 index。唯一例外:列表是静态的(永远不会增删排序),用 index 没问题。
4. 综合示例:待办列表
把列表渲染、事件、条件样式组合起来:
function TodoList({ todos, onToggle }) {
return (
<ul>
{/* 列表渲染 + 事件 + 条件渲染 全部组合 */}
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => onToggle(todo.id)}
/>
<span style={{
textDecoration: todo.done ? "line-through" : "none",
color: todo.done ? "#999" : "inherit"
}}>
{todo.text}
</span>
</li>
))}
</ul>
);
}这个例子浓缩了日常开发的套路:从 props 拿数据 → map 渲染 → 每项给稳定 key → 绑定事件 → 事件里调父组件回调改数据。React 80% 的页面都是这个模式。
5. 嵌套列表
列表嵌套时,每一层都要给自己的元素加 key,key 只要在同级唯一即可(不同层可以重复):
// 嵌套列表:外层和内层都需要 key
function Menu({ menuData }) {
return (
<ul>
{menuData.map(section => (
<li key={section.id}>
<h3>{section.title}</h3>
<ul>
{section.items.map(item => (
<li key={item.id}>
<a href={item.url}>{item.label}</a>
</li>
))}
</ul>
</li>
))}
</ul>
);
}6. 配合 filter / sort
实际项目里,列表往往要先过滤或排序再渲染。直接用 JS 的 filter、sort、reduce 链式调用即可:
function ProductList({ products }) {
// 链式调用:先 filter 再 map
const inStock = products
.filter(p => p.stock > 0)
.map(p => (
<div key={p.id} className="product">
<h3>{p.name}</h3>
<p>价格:{p.price} 元 (库存 {p.stock})</p>
</div>
));
return <div className="grid">{inStock}</div>;
}注意 sort 会修改原数组,而 React 的 state 是不可变的——所以要先 [...products] 复制一份再 sort,或用 toSorted(新方法,返回新数组)。
7. key 的常见误区
- key 不是给开发者用的:你不能在组件里通过
props.key读取 key——它是 React 内部的提示,不会传给组件。 - key 要放在最外层元素上:如果 map 返回
<Fragment>,key 放在 Fragment 上,不是内部元素上。 - key 全局唯一是误解:key 只要在兄弟节点之间唯一即可,不同列表可以用相同的 key。
小结
列表渲染 = .map() 返回 JSX 数组,每个元素必须带稳定唯一的 key(优先用数据 id,绝不用 index)。掌握 key,你就掌握了 React 性能优化的第一道关。下一篇看 props 透传太深的解药——Context。
← 上一篇 React 事件处理
下一篇 Context 跨层传递 →