列表渲染
使用 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 来匹配原始树和新树的子元素:
- 如果 Key 相同,React 会复用和更新该组件
- 如果 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>
</>
);
}
相关概念
这篇文章有帮助吗?
Previous / Next
Related Links