列表渲染与 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 判断"哪些元素是新的、哪些只是位置变了、哪些被删了",从而只更新变化的部分,而不是整个列表重新渲染。

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]

所以规则: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 的 filtersortreduce 链式调用即可:

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 的常见误区

小结

列表渲染 = .map() 返回 JSX 数组,每个元素必须带稳定唯一的 key(优先用数据 id,绝不用 index)。掌握 key,你就掌握了 React 性能优化的第一道关。下一篇看 props 透传太深的解药——Context

← 上一篇 React 事件处理

下一篇 Context 跨层传递

✈️💬