列表渲染

使用 JavaScript 的 map() 方法渲染多个组件

什么是列表渲染?

在 React 中,你可以使用 JavaScript 的 map() 方法 将数组转换为 JSX 元素数组,从而渲染列表数据。

TypeScript
const products = [
  { id: 1, name: '苹果', price: 5.5 },
  { id: 2, name: '香蕉', price: 3.0 },
  { id: 3, name: '橙子', price: 4.5 },
];

function ProductList() {
  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>
          {product.name} - ¥{product.price}
        </li>
      ))}
    </ul>
  );
}
核心概念

React 中的列表渲染就是使用 JavaScript 的数组方法,而不是特殊的模板语法。 这使得列表渲染非常灵活和强大。

渲染列表

基本用法

TypeScript
const fruits = [
  { id: 'apple', name: '苹果' },
  { id: 'banana', name: '香蕉' },
  { id: 'orange', name: '橙子' },
];

function FruitList() {
  return (
    <ul>
      {fruits.map((fruit) => (
        <li key={fruit.id}>
          {fruit.name}
        </li>
      ))}
    </ul>
  );
}

渲染对象数组

TypeScript
const users = [
  { id: 1, name: 'Alice', age: 25 },
  { id: 2, name: 'Bob', age: 30 },
  { id: 3, name: 'Charlie', age: 35 },
];

function UserList() {
  return (
    <div>
      {users.map(user => (
        <div key={user.id} className="user-card">
          <h3>{user.name}</h3>
          <p>年龄: {user.age}</p>
        </div>
      ))}
    </div>
  );
}

渲染组件列表

TypeScript
function UserCard({ user }) {
  return (
    <div className="user-card">
      <h3>{user.name}</h3>
      <p>年龄: {user.age}</p>
    </div>
  );
}

function UserList({ users }) {
  return (
    <div className="user-list">
      {users.map(user => (
        <UserCard key={user.id} user={user} />
      ))}
    </div>
  );
}

理解 Key

为什么需要 Key?

Key 帮助 React 识别哪些项发生了改变、添加或删除。 每个列表项都应该有一个唯一的 Key。

TypeScript
const numbers = [1, 2, 3, 4, 5];

function NumberList() {
  return (
    <ul>
      {numbers.map((number) => (
        <li key={number.toString()}>
          {number}
        </li>
      ))}
    </ul>
  );
}

Key 的规则

1. Key 必须是唯一的

TypeScript
// ✅ 正确:使用唯一 ID
{items.map(item => (
  <li key={item.id}>{item.name}</li>
))}

// ❌ 错误:使用索引(即使在静态列表中也不推荐)
{items.map((item, index) => (
  <li key={index}>{item.name}</li>
))}

// ✅ 正确:使用数据的唯一标识符
{items.map(item => (
  <li key={item.slug}>{item.name}</li>
))}

2. Key 应该稳定

TypeScript
// ❌ 错误:不稳定的 key(随机数)
{items.map(item => (
  <li key={Math.random()}>{item.name}</li>
))}

// ❌ 错误:不稳定的 key(时间戳)
{items.map(item => (
  <li key={Date.now()}>{item.name}</li>
))}

// ✅ 正确:稳定的 key
{items.map(item => (
  <li key={item.id}>{item.name}</li>
))}

3. Key 不要在组件内部访问

TypeScript
// ❌ 错误:不要在组件内部使用 key
function ListItem({ key, item }) {
  return <li>{key}: {item.name}</li>;
}

// ✅ 正确:使用 item.id
function ListItem({ item }) {
  return <li>{item.id}: {item.name}</li>;
}
重要

不要使用索引作为 Key,除非:

  • 列表是静态的(永远不会改变)
  • 列表项没有唯一标识符
  • 你完全了解潜在的性能和状态问题

最佳实践:始终使用数据中的唯一标识符(如 ID、slug 等)作为 Key。 如果没有唯一标识符,考虑在数据中添加一个(例如使用 crypto.randomUUID())。

为什么 Key 很重要?

示例:没有正确使用 Key

TypeScript
function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: '学习 React', done: false },
    { id: 2, text: '写代码', done: false },
  ]);

  const toggle = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id
        ? { ...todo, done: !todo.done }
        : todo
    ));
  };

  return (
    <ul>
      {todos.map((todo, index) => (
        <TodoItem
          key={index}  // ❌ 使用索引
          todo={todo}
          onToggle={() => toggle(todo.id)}
        />
      ))}
    </ul>
  );
}

当你重新排序列表时,使用索引作为 Key 会导致问题:

TypeScript
// 初始状态
[{ id: 1, text: '学习 React', done: false },
 { id: 2, text: '写代码', done: false }]

// 使用索引作为 key:
// <TodoItem key=0 todo={id: 1} />
// <TodoItem key=1 todo={id: 2} />

// 重新排序后:
[{ id: 2, text: '写代码', done: false },
 { id: 1, text: '学习 React', done: false }]

// React 认为还是同样的组件(因为 key 还是 0 和 1):
// <TodoItem key=0 todo={id: 2} />  // React 复用了组件!
// <TodoItem key=1 todo={id: 1} />  // 导致状态混乱

正确使用 ID 作为 Key

TypeScript
{todos.map(todo => (
  <TodoItem
    key={todo.id}  // ✅ 使用唯一 ID
    todo={todo}
    onToggle={() => toggle(todo.id)}
  />
))}
React 如何使用 Key

React 使用 Key 来匹配原始树和新树的子元素:

  1. 如果 Key 相同,React 会复用和更新该组件
  2. 如果 Key 不同,React 会销毁旧组件并创建新组件

正确的 Key 可以帮助 React 高效地更新 DOM。

提取组件

将列表项提取为组件

TypeScript
function UserItem({ user }) {
  return (
    <li className="user-item">
      <img src={user.avatar} alt={user.name} />
      <div>
        <h3>{user.name}</h3>
        <p>{user.email}</p>
      </div>
    </li>
  );
}

function UserList({ users }) {
  return (
    <ul className="user-list">
      {users.map(user => (
        <UserItem key={user.id} user={user} />
      ))}
    </ul>
  );
}

Key 放在哪里?

TypeScript
// ✅ 正确:key 在 map() 返回的元素上
{items.map(item => (
  <UserItem key={item.id} item={item} />
))}

// ❌ 错误:key 在子组件内部
function UserItem({ item }) {
  return <li key={item.id}>{item.name}</li>;
}

// ❌ 错误:多个元素需要包装
{items.map(item => (
  <>
    <UserItem key={item.id} item={item} />
  </>
))}
最佳实践

总是在 map() 调用内部的最外层 JSX 元素上指定 Key, 而不是在子组件内部。

过滤列表

使用 filter

TypeScript
const tasks = [
  { id: 1, text: '学习 React', completed: false },
  { id: 2, text: '写代码', completed: true },
  { id: 3, text: '调试', completed: false },
];

function TaskList() {
  const [showCompleted, setShowCompleted] = useState(false);

  const visibleTasks = showCompleted
    ? tasks
    : tasks.filter(task => !task.completed);

  return (
    <div>
      <label>
        <input
          type="checkbox"
          checked={showCompleted}
          onChange={e => setShowCompleted(e.target.checked)}
        />
        显示已完成
      </label>

      <ul>
        {visibleTasks.map(task => (
          <li key={task.id}>
            {task.text}
          </li>
        ))}
      </ul>
    </div>
  );
}

搜索过滤

TypeScript
function UserList({ users }) {
  const [searchTerm, setSearchTerm] = useState('');

  const filteredUsers = users.filter(user =>
    user.name.toLowerCase().includes(searchTerm.toLowerCase())
  );

  return (
    <div>
      <input
        type="text"
        value={searchTerm}
        onChange={e => setSearchTerm(e.target.value)}
        placeholder="搜索用户..."
      />

      <ul>
        {filteredUsers.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

排序列表

基本排序

TypeScript
const products = [
  { id: 1, name: '苹果', price: 5.5 },
  { id: 2, name: '香蕉', price: 3.0 },
  { id: 3, name: '橙子', price: 4.5 },
];

function ProductList({ products }) {
  const [sortBy, setSortBy] = useState('price');

  const sortedProducts = [...products].sort((a, b) => {
    if (sortBy === 'price') {
      return a.price - b.price;
    }
    return a.name.localeCompare(b.name);
  });

  return (
    <div>
      <select value={sortBy} onChange={e => setSortBy(e.target.value)}>
        <option value="price">按价格</option>
        <option value="name">按名称</option>
      </select>

      <ul>
        {sortedProducts.map(product => (
          <li key={product.id}>
            {product.name} - ¥{product.price}
          </li>
        ))}
      </ul>
    </div>
  );
}
注意

sort() 方法会修改原数组。使用展开运算符 [...products] 创建新数组后再排序,避免修改 props。

修改列表

添加项

TypeScript
function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: '学习 React' }
  ]);

  const addTodo = (text) => {
    const newTodo = {
      id: Date.now(),
      text
    };

    setTodos([...todos, newTodo]);
  };

  return (
    <div>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>

      <button onClick={() => addTodo('新任务')}>
        添加任务
      </button>
    </div>
  );
}

删除项

TypeScript
function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: '学习 React' },
    { id: 2, text: '写代码' }
  ]);

  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          {todo.text}
          <button onClick={() => deleteTodo(todo.id)}>
            删除
          </button>
        </li>
      ))}
    </ul>
  );
}

更新项

TypeScript
function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: '学习 React', done: false },
    { id: 2, text: '写代码', done: false }
  ]);

  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id
        ? { ...todo, done: !todo.done }
        : todo
    ));
  };

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <input
            type="checkbox"
            checked={todo.done}
            onChange={() => toggleTodo(todo.id)}
          />
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

插入项

TypeScript
function insertItem(array, index, newItem) {
  return [
    ...array.slice(0, index),
    newItem,
    ...array.slice(index)
  ];
}

const [items, setItems] = useState([1, 2, 3, 4]);

// 在索引 1 处插入
setItems(insertItem(items, 1, 99));
// 结果: [1, 99, 2, 3, 4]

常见模式

带索引的列表

TypeScript
function NumberedList({ items }) {
  return (
    <ol>
      {items.map((item, index) => (
        <li key={item.id}>
          {index + 1}. {item.text}
        </li>
      ))}
    </ol>
  );
}

分组列表

TypeScript
const users = [
  { id: 1, name: 'Alice', role: 'admin' },
  { id: 2, name: 'Bob', role: 'user' },
  { id: 3, name: 'Charlie', role: 'admin' },
];

function GroupedUserList({ users }) {
  const groups = users.reduce((acc, user) => {
    const role = user.role;
    if (!acc[role]) {
      acc[role] = [];
    }
    acc[role].push(user);
    return acc;
  }, {});

  return (
    <div>
      {Object.entries(groups).map(([role, users]) => (
        <div key={role}>
          <h2>{role.toUpperCase()}S</h2>
          <ul>
            {users.map(user => (
              <li key={user.id}>{user.name}</li>
            ))}
          </ul>
        </div>
      ))}
    </div>
  );
}

嵌套列表

TypeScript
const categories = [
  {
    id: 1,
    name: '水果',
    items: [
      { id: 11, name: '苹果' },
      { id: 12, name: '香蕉' }
    ]
  },
  {
    id: 2,
    name: '蔬菜',
    items: [
      { id: 21, name: '胡萝卜' },
      { id: 22, name: '白菜' }
    ]
  }
];

function CategoryList({ categories }) {
  return (
    <div>
      {categories.map(category => (
        <div key={category.id}>
          <h2>{category.name}</h2>
          <ul>
            {category.items.map(item => (
              <li key={item.id}>{item.name}</li>
            ))}
          </ul>
        </div>
      ))}
    </div>
  );
}
嵌套列表的 Key

对于嵌套列表,确保每个级别的列表项都有唯一的 Key。 内层和外层的 Key 可以使用不同的属性。

性能优化

使用 useMemo 缓存

TypeScript
import { useMemo } from 'react';

function ExpensiveList({ items, filter }) {
  const filteredItems = useMemo(() => {
    return items.filter(item =>
      item.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [items, filter]);

  return (
    <ul>
      {filteredItems.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

虚拟化长列表

对于包含数千项的列表,考虑使用虚拟化库:

TypeScript
// 使用 react-window 或 react-virtualized
import { FixedSizeList } from 'react-window';

function VirtualizedList({ items }) {
  return (
    <FixedSizeList
      height={400}
      itemCount={items.length}
      itemSize={35}
      width="100%"
    >
      {({ index, style }) => (
        <div style={style}>
          {items[index].name}
        </div>
      )}
    </FixedSizeList>
  );
}
什么时候需要虚拟化?

当列表包含数百或数千项时,一次性渲染所有项会导致性能问题。 虚拟化只渲染可见的项,大大提高性能。

最佳实践

1. 总是使用稳定的 Key

TypeScript
// ✅ 好:使用数据中的唯一 ID
{items.map(item => (
  <Component key={item.id} item={item} />
))}

// ❌ 差:使用索引(如果列表会改变)
{items.map((item, index) => (
  <Component key={index} item={item} />
))}

2. Key 在 map 返回的元素上

TypeScript
// ✅ 正确
{items.map(item => (
  <li key={item.id}>{item.name}</li>
))}

// ❌ 错误
{items.map(item => {
  return <li key={item.id}>{item.name}</li>;
})}

3. 不要创建新的数组

TypeScript
// ✅ 好:直接使用 map
{items.map(item => <Item key={item.id} item={item} />)}

// ❌ 差:不必要的 filter(如果不需要过滤)
{items.filter(() => true).map(item => <Item key={item.id} item={item} />)}

4. 提取重复的列表项

TypeScript
// ✅ 好:提取为组件
function UserItem({ user }) {
  return <li>{user.name}</li>;
}

{users.map(user => (
  <UserItem key={user.id} user={user} />
))}

常见问题

列表项没有更新?

确保你在修改数组时创建了新数组:

TypeScript
// ❌ 错误:直接修改
items.push(newItem);
setItems(items);

// ✅ 正确:创建新数组
setItems([...items, newItem]);

警告:Each child should have a unique key

TypeScript
// ❌ 错误:没有 key
{items.map(item => (
  <li>{item.name}</li>
))}

// ✅ 正确:添加 key
{items.map(item => (
  <li key={item.id}>{item.name}</li>
))}

列表顺序混乱?

检查是否使用了稳定的 Key。使用索引作为 Key 会导致问题。

如何处理动态列表?

TypeScript
function DynamicList() {
  const [items, setItems] = useState([]);

  const addItem = () => {
    setItems([
      ...items,
      { id: Date.now(), name: '新项' }
    ]);
  };

  return (
    <>
      <button onClick={addItem}>添加</button>
      <ul>
        {items.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </>
  );
}

相关概念

这篇文章有帮助吗?