React列表与Key

列表渲染是React中最常见的操作之一,而Key是列表渲染中至关重要的概念。本章将深入讲解如何使用map()函数渲染列表,以及为什么需要Key和如何正确使用Key。

为什么需要列表渲染?

在实际应用中,我们经常需要根据数组数据动态生成一组相似的元素。例如:用户列表、商品列表、评论列表等。

列表渲染示例


// 数据数组
const users = [
  { id: 1, name: '张三', age: 25 },
  { id: 2, name: '李四', age: 30 },
  { id: 3, name: '王五', age: 28 }
];

// 列表渲染
function UserList() {
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>
          {user.name} - {user.age}岁
        </li>
      ))}
    </ul>
  );
}
                        

使用map()函数渲染列表

JavaScript的map()函数是React中渲染列表的主要工具,它会遍历数组并为每个元素返回一个新的React元素。

基本列表渲染

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

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

  return <ul>{listItems}</ul>;
}
                                    
复杂数据渲染

const products = [
  { id: 101, name: 'iPhone', price: 6999 },
  { id: 102, name: 'MacBook', price: 12999 },
  { id: 103, name: 'iPad', price: 3299 }
];

function ProductList() {
  return (
    <div className="products">
      {products.map(product => (
        <div key={product.id} className="product">
          <h3>{product.name}</h3>
          <p>价格: ¥{product.price}</p>
        </div>
      ))}
    </div>
  );
}
                                    

map()函数的其他用法


function EnhancedList() {
  const items = [
    { id: 1, title: '任务1', completed: true },
    { id: 2, title: '任务2', completed: false },
    { id: 3, title: '任务3', completed: true }
  ];

  // 使用map计算摘要
  const total = items.length;
  const completedCount = items.filter(item => item.completed).length;

  return (
    <div>
      <div className="summary">
        总计: {total} | 已完成: {completedCount}
      </div>

      {/* 使用map渲染列表 */}
      <ul className="item-list">
        {items.map(item => {
          // 可以在这里添加复杂的逻辑
          const statusClass = item.completed ? 'completed' : 'pending';

          return (
            <li key={item.id} className={`item ${statusClass}`}>
              <span>{item.title}</span>
              <span className="status">
                {item.completed ? '✓ 已完成' : '○ 待完成'}
              </span>
            </li>
          );
        })}
      </ul>

      {/* 使用map生成选项 */}
      <select>
        {items.map(item => (
          <option key={item.id} value={item.id}>
            {item.title}
          </option>
        ))}
      </select>
    </div>
  );
}
                    

什么是Key?

Key是React用于识别列表中元素唯一性的特殊字符串属性。它帮助React跟踪哪些元素被添加、修改或删除。

元素A
key="a1"
元素B
key="b2"
元素C
key="c3"

为什么Key如此重要?

性能优化

Key帮助React识别元素,从而实现高效的DOM更新。当列表变化时,React可以准确地知道哪些元素需要更新,而不是重新渲染整个列表。


// 没有Key时,React需要重新渲染整个列表
// 有Key时,React只更新变化的元素
                                    
状态保持

Key确保组件的状态在重新渲染时得到保持。如果没有Key,组件的状态可能会丢失或混乱。


// 输入框的值、复选框的状态等
// 在列表重新排序时能够正确保持
                                    
元素识别

Key是React识别列表中每个元素的唯一标识。即使元素内容相同,Key也能帮助React区分它们。


// 相同内容的两个元素
// <li>任务1</li>
// <li>任务1</li>
// Key让React知道这是两个不同的元素
                                    
动画效果

正确的Key可以确保CSS过渡和动画效果正常工作,特别是在列表重新排序时。


// 列表项添加/删除时的动画
// 列表重新排序时的过渡效果
// 都依赖于稳定的Key
                                    

如何选择Key

Key类型 示例 适用场景 注意事项
数据ID key={item.id} 有唯一ID的数据(推荐) 最佳选择,稳定且唯一
索引 key={index} 静态列表、简单展示 不推荐,可能导致问题
组合Key key={`${type}-${id}`} 不同类型数据混合 确保组合后的唯一性
生成ID key={nanoid()} 没有ID的本地数据 每次渲染都不同,不推荐
时间戳 key={Date.now()} 测试、临时数据 每次渲染都不同,不推荐

Key选择示例


// ✅ 推荐:使用数据中的唯一ID
const users = [
  { id: 'u1', name: '张三' },
  { id: 'u2', name: '李四' }
];

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

// ⚠️ 谨慎使用:索引作为Key(静态列表可用)
const staticItems = ['苹果', '香蕉', '橙子'];
staticItems.map((item, index) => (
  <li key={index}>{item}</li>
));

// ✅ 推荐:组合Key
const mixedData = [
  { type: 'user', id: 1, name: '张三' },
  { type: 'post', id: 1, title: '文章1' }
];

mixedData.map(item => (
  <div key={`${item.type}-${item.id}`}>
    {item.name || item.title}
  </div>
));

// ❌ 避免:使用随机Key
items.map(item => (
  <div key={Math.random()}>{item.name}</div> // 每次渲染Key都不同
));

// ❌ 避免:使用不稳定的Key
items.map((item, index) => (
  <div key={item.name}>{item.name}</div> // 可能重复
));
                    
重要警告:不要使用索引作为Key的情况

当列表有以下情况时,绝对不要使用索引作为Key

  • 列表可以重新排序(排序、过滤、搜索)
  • 列表项有状态(输入框、复选框等)
  • 列表项可以被添加或删除
  • 列表项有复杂的交互或动画

Key的错误使用示例


// ❌ 错误示例:使用索引作为Key,删除项目时会导致状态混乱
function TodoList() {
  const [todos, setTodos] = useState([
    { text: '学习React', completed: false },
    { text: '写文档', completed: false },
    { text: '测试代码', completed: false }
  ]);

  const deleteTodo = (index) => {
    setTodos(todos.filter((_, i) => i !== index));
  };

  return (
    <ul>
      {todos.map((todo, index) => (
        <TodoItem
          key={index}  // ❌ 问题:删除项目后,索引会变化
          todo={todo}
          onDelete={() => deleteTodo(index)}
        />
      ))}
    </ul>
  );
}

// 删除第一个项目后:
// 原来索引1的项目现在变成了索引0
// 但React认为索引0还是原来的项目
// 导致状态错乱
                                    


// ✅ 正确示例:使用唯一ID作为Key
function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: '学习React', completed: false },
    { id: 2, text: '写文档', completed: false },
    { id: 3, text: '测试代码', completed: false }
  ]);

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

  return (
    <ul>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}  // ✅ 正确:使用唯一ID
          todo={todo}
          onDelete={() => deleteTodo(todo.id)}
        />
      ))}
    </ul>
  );
}

// 现在删除项目时,React能够正确识别哪个项目被删除
// 其他项目的状态得到保持
                                    

列表渲染实时演示

// 代码将在这里显示

列表渲染性能优化

虚拟化长列表

对于包含大量数据的列表(成千上万条),一次性渲染所有项目会导致性能问题。解决方案是使用虚拟化技术,只渲染可见区域的项目。

使用react-window库实现虚拟化

import { FixedSizeList as List } from 'react-window';

const Row = ({ index, style }) => (
  <div style={style}>
    第 {index} 行: {items[index].name}
  </div>
);

function VirtualizedList() {
  return (
    <List
      height={400}      // 列表高度
      width={300}       // 列表宽度
      itemCount={10000} // 总项目数
      itemSize={35}     // 每个项目高度
    >
      {Row}
    </List>
  );
}
                            

其他性能优化技巧


import React, { useMemo, useCallback } from 'react';

function OptimizedList({ items, filterText, sortBy }) {
  // 使用useMemo缓存过滤和排序结果
  const processedItems = useMemo(() => {
    console.log('处理列表数据...');

    let result = [...items];

    // 过滤
    if (filterText) {
      result = result.filter(item =>
        item.name.toLowerCase().includes(filterText.toLowerCase())
      );
    }

    // 排序
    if (sortBy) {
      result.sort((a, b) => {
        if (sortBy === 'name') return a.name.localeCompare(b.name);
        if (sortBy === 'date') return new Date(b.date) - new Date(a.date);
        return 0;
      });
    }

    return result;
  }, [items, filterText, sortBy]); // 依赖项变化时重新计算

  // 使用useCallback缓存事件处理函数
  const handleItemClick = useCallback((id) => {
    console.log('点击项目:', id);
  }, []);

  // 避免在渲染函数中创建新数组
  const itemIds = useMemo(() =>
    processedItems.map(item => item.id),
    [processedItems]
  );

  return (
    <div>
      <div className="summary">
        显示 {processedItems.length} / {items.length} 个项目
      </div>

      <ul className="optimized-list">
        {processedItems.map(item => (
          <MemoizedListItem
            key={item.id}
            item={item}
            onClick={handleItemClick}
          />
        ))}
      </ul>
    </div>
  );
}

// 使用React.memo优化列表项组件
const MemoizedListItem = React.memo(function ListItem({ item, onClick }) {
  return (
    <li onClick={() => onClick(item.id)}>
      {item.name}
    </li>
  );
});
                    

嵌套列表与复杂数据结构


function NestedList() {
  const categories = [
    {
      id: 1,
      name: '电子产品',
      products: [
        { id: 101, name: 'iPhone', price: 6999 },
        { id: 102, name: 'MacBook', price: 12999 }
      ]
    },
    {
      id: 2,
      name: '图书',
      products: [
        { id: 201, name: 'React入门', price: 59 },
        { id: 202, name: 'JavaScript高级', price: 89 }
      ]
    }
  ];

  return (
    <div className="nested-list">
      {categories.map(category => (
        <div key={category.id} className="category">
          <h3>{category.name}</h3>

          <ul className="product-list">
            {category.products.map(product => (
              <li key={product.id} className="product">
                <span className="name">{product.name}</span>
                <span className="price">¥{product.price}</span>
              </li>
            ))}
          </ul>
        </div>
      ))}
    </div>
  );
}
                    

动态列表操作


import React, { useState } from 'react';

function DynamicList() {
  const [items, setItems] = useState([
    { id: 1, text: '第一个项目' },
    { id: 2, text: '第二个项目' },
    { id: 3, text: '第三个项目' }
  ]);

  const [newItemText, setNewItemText] = useState('');

  // 添加项目
  const addItem = () => {
    if (!newItemText.trim()) return;

    const newItem = {
      id: Date.now(), // 使用时间戳作为临时ID
      text: newItemText
    };

    setItems([...items, newItem]);
    setNewItemText('');
  };

  // 删除项目
  const removeItem = (id) => {
    setItems(items.filter(item => item.id !== id));
  };

  // 移动项目
  const moveItem = (fromIndex, toIndex) => {
    const newItems = [...items];
    const [removed] = newItems.splice(fromIndex, 1);
    newItems.splice(toIndex, 0, removed);
    setItems(newItems);
  };

  // 更新项目
  const updateItem = (id, newText) => {
    setItems(items.map(item =>
      item.id === id ? { ...item, text: newText } : item
    ));
  };

  return (
    <div className="dynamic-list">
      <h3>动态列表操作</h3>

      <div className="add-form">
        <input
          type="text"
          value={newItemText}
          onChange={(e) => setNewItemText(e.target.value)}
          placeholder="输入新项目..."
          onKeyPress={(e) => e.key === 'Enter' && addItem()}
        />
        <button onClick={addItem}>添加</button>
      </div>

      <ul className="item-list">
        {items.map((item, index) => (
          <li key={item.id} className="list-item">
            <div className="item-content">
              <span className="item-index">#{index + 1}</span>
              <input
                type="text"
                value={item.text}
                onChange={(e) => updateItem(item.id, e.target.value)}
                className="item-input"
              />
            </div>

            <div className="item-actions">
              {index > 0 && (
                <button onClick={() => moveItem(index, index - 1)}>
                  ↑ 上移
                </button>
              )}
              {index < items.length - 1 && (
                <button onClick={() => moveItem(index, index + 1)}>
                  ↓ 下移
                </button>
              )}
              <button
                onClick={() => removeItem(item.id)}
                className="delete-btn"
              >
                删除
              </button>
            </div>
          </li>
        ))}
      </ul>

      <div className="list-info">
        <p>总项目数: {items.length}</p>
        <p>提示:可以编辑、删除、移动项目</p>
      </div>
    </div>
  );
}
                    

列表渲染最佳实践

推荐做法
  • 为每个列表项提供稳定、唯一的Key
  • 优先使用数据中的ID作为Key
  • 使用React.memo优化列表项组件
  • 对长列表使用虚拟化技术
  • 使用useMemo缓存计算密集型操作
  • 将列表项拆分为独立的组件
  • 为复杂列表添加空状态和加载状态
避免做法
  • 避免使用索引作为动态列表的Key
  • 不要使用随机值或时间戳作为Key
  • 避免在渲染函数中创建新数组或对象
  • 不要忘记为嵌套列表的每个层级提供Key
  • 避免在列表项中使用内联函数
  • 不要一次性渲染超长列表
  • 避免在列表渲染中进行昂贵计算

完整示例:任务管理面板


import React, { useState, useMemo, useCallback } from 'react';

function TaskManagementPanel() {
  const [tasks, setTasks] = useState([
    { id: 1, title: '完成React教程', status: 'todo', priority: 'high', assignee: '张三' },
    { id: 2, title: '修复登录Bug', status: 'in-progress', priority: 'high', assignee: '李四' },
    { id: 3, title: '编写文档', status: 'done', priority: 'medium', assignee: '王五' },
    { id: 4, title: '优化性能', status: 'todo', priority: 'low', assignee: '赵六' },
    { id: 5, title: '团队会议', status: 'in-progress', priority: 'medium', assignee: '张三' }
  ]);

  const [filterStatus, setFilterStatus] = useState('all');
  const [sortBy, setSortBy] = useState('priority');
  const [searchText, setSearchText] = useState('');

  // 过滤和排序任务
  const filteredTasks = useMemo(() => {
    let result = tasks.filter(task => {
      // 状态过滤
      if (filterStatus !== 'all' && task.status !== filterStatus) {
        return false;
      }

      // 搜索过滤
      if (searchText && !task.title.toLowerCase().includes(searchText.toLowerCase())) {
        return false;
      }

      return true;
    });

    // 排序
    result.sort((a, b) => {
      if (sortBy === 'priority') {
        const priorityOrder = { high: 3, medium: 2, low: 1 };
        return priorityOrder[b.priority] - priorityOrder[a.priority];
      }
      if (sortBy === 'title') {
        return a.title.localeCompare(b.title);
      }
      if (sortBy === 'assignee') {
        return a.assignee.localeCompare(b.assignee);
      }
      return 0;
    });

    return result;
  }, [tasks, filterStatus, sortBy, searchText]);

  // 添加新任务
  const addTask = useCallback((newTask) => {
    setTasks([...tasks, { id: Date.now(), ...newTask }]);
  }, [tasks]);

  // 更新任务状态
  const updateTaskStatus = useCallback((taskId, newStatus) => {
    setTasks(tasks.map(task =>
      task.id === taskId ? { ...task, status: newStatus } : task
    ));
  }, [tasks]);

  // 删除任务
  const deleteTask = useCallback((taskId) => {
    setTasks(tasks.filter(task => task.id !== taskId));
  }, [tasks]);

  // 统计数据
  const stats = useMemo(() => {
    return {
      total: tasks.length,
      todo: tasks.filter(t => t.status === 'todo').length,
      inProgress: tasks.filter(t => t.status === 'in-progress').length,
      done: tasks.filter(t => t.status === 'done').length,
      highPriority: tasks.filter(t => t.priority === 'high').length
    };
  }, [tasks]);

  return (
    <div className="task-management-panel">
      <h2>任务管理面板</h2>

      {/* 控制面板 */}
      <div className="control-panel">
        <div className="search-box">
          <input
            type="text"
            placeholder="搜索任务..."
            value={searchText}
            onChange={(e) => setSearchText(e.target.value)}
          />
        </div>

        <div className="filter-controls">
          <select
            value={filterStatus}
            onChange={(e) => setFilterStatus(e.target.value)}
          >
            <option value="all">所有状态</option>
            <option value="todo">待办</option>
            <option value="in-progress">进行中</option>
            <option value="done">已完成</option>
          </select>

          <select
            value={sortBy}
            onChange={(e) => setSortBy(e.target.value)}
          >
            <option value="priority">按优先级排序</option>
            <option value="title">按标题排序</option>
            <option value="assignee">按负责人排序</option>
          </select>
        </div>
      </div>

      {/* 统计信息 */}
      <div className="stats-panel">
        <div className="stat-item total">
          <h4>总计</h4>
          <p>{stats.total}</p>
        </div>
        <div className="stat-item todo">
          <h4>待办</h4>
          <p>{stats.todo}</p>
        </div>
        <div className="stat-item in-progress">
          <h4>进行中</h4>
          <p>{stats.inProgress}</p>
        </div>
        <div className="stat-item done">
          <h4>已完成</h4>
          <p>{stats.done}</p>
        </div>
      </div>

      {/* 任务列表 */}
      <div className="task-list-container">
        {filteredTasks.length === 0 ? (
          <div className="empty-state">
            <i className="fas fa-tasks fa-3x"></i>
            <h3>没有任务</h3>
            <p>尝试更改筛选条件或添加新任务</p>
          </div>
        ) : (
          <ul className="task-list">
            {filteredTasks.map(task => (
              <TaskItem
                key={task.id}
                task={task}
                onUpdateStatus={updateTaskStatus}
                onDelete={deleteTask}
              />
            ))}
          </ul>
        )}
      </div>

      {/* 添加任务表单 */}
      <AddTaskForm onAddTask={addTask} />
    </div>
  );
}

// 任务项组件
const TaskItem = React.memo(function TaskItem({ task, onUpdateStatus, onDelete }) {
  const statusColors = {
    todo: '#ff6b6b',
    'in-progress': '#4ecdc4',
    done: '#45b7d1'
  };

  const priorityColors = {
    high: '#ff6b6b',
    medium: '#feca57',
    low: '#48dbfb'
  };

  return (
    <li className="task-item">
      <div className="task-main">
        <div className="task-header">
          <h4 className="task-title">{task.title}</h4>
          <div className="task-actions">
            <select
              value={task.status}
              onChange={(e) => onUpdateStatus(task.id, e.target.value)}
              style={color: statusColors[task.status]}
            >
              <option value="todo">待办</option>
              <option value="in-progress">进行中</option>
              <option value="done">已完成</option>
            </select>
            <button
              onClick={() => onDelete(task.id)}
              className="delete-btn"
            >
              删除
            </button>
          </div>
        </div>

        <div className="task-meta">
          <span
            className="priority-badge"
            style={backgroundColor: priorityColors[task.priority]}
          >
            {task.priority === 'high' ? '高' :
             task.priority === 'medium' ? '中' : '低'}优先级
          </span>
          <span className="assignee">负责人: {task.assignee}</span>
        </div>
      </div>

      <div
        className="status-indicator"
        style={backgroundColor: statusColors[task.status]}
      ></div>
    </li>
  );
});

// 添加任务表单组件
function AddTaskForm({ onAddTask }) {
  const [newTask, setNewTask] = useState({
    title: '',
    priority: 'medium',
    assignee: ''
  });

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!newTask.title.trim()) return;

    onAddTask({
      ...newTask,
      status: 'todo'
    });

    setNewTask({
      title: '',
      priority: 'medium',
      assignee: ''
    });
  };

  return (
    <form onSubmit={handleSubmit} className="add-task-form">
      <h4>添加新任务</h4>
      <div className="form-fields">
        <input
          type="text"
          placeholder="任务标题"
          value={newTask.title}
          onChange={(e) => setNewTask({...newTask, title: e.target.value})}
          required
        />
        <select
          value={newTask.priority}
          onChange={(e) => setNewTask({...newTask, priority: e.target.value})}
        >
          <option value="low">低优先级</option>
          <option value="medium">中优先级</option>
          <option value="high">高优先级</option>
        </select>
        <input
          type="text"
          placeholder="负责人"
          value={newTask.assignee}
          onChange={(e) => setNewTask({...newTask, assignee: e.target.value})}
        />
        <button type="submit">添加任务</button>
      </div>
    </form>
  );
}
                    

本章要点总结

  • 使用map()函数渲染列表,为每个列表项返回React元素
  • Key是React识别列表中元素的唯一标识,对性能和状态保持至关重要
  • 优先使用数据中的唯一ID作为Key,避免使用索引作为动态列表的Key
  • Key应该在列表的兄弟元素之间保持唯一,但全局不要求唯一
  • 对于长列表,使用虚拟化技术(如react-window)提高性能
  • 使用React.memouseMemouseCallback优化列表性能
  • 为嵌套列表的每个层级都需要提供合适的Key