React: 条件渲染与列表渲染
最后更新:2026-08-26
条件渲染就像 交通信号灯——根据状态决定显示什么;列表渲染就像 流水线生产——一份数据清单,批量生成相同的 UI 组件。
1. 你将学到
- 使用三元运算符
? :控制显示内容 - 使用
&&和||简化条件渲染 - 使用
.map()渲染列表 - key 属性的重要性和使用规则
- 空状态和加载状态的处理
2. 一个电商平台的前端故事
(1) 痛点:商品列表的 6 种状态
Alice 在做一个电商平台的商品列表页。这个页面有 6 种显示状态,每次状态变化都要手动操作 DOM:
JAVASCRIPT
function renderProductList(state) {
const container = document.getElementById('product-list')
container.innerHTML = ''
if (state.isLoading) {
container.innerHTML = '<div class="loading">加载中...</div>'
} else if (state.error) {
container.innerHTML = '<div class="error">加载失败</div>'
} else if (!state.isLoggedIn) {
container.innerHTML = '<div>请登录</div>'
} else if (state.products.length === 0) {
container.innerHTML = '<div>暂无商品</div>'
} else {
let html = ''
state.products.forEach(p => {
html += `<div class="card"><h3>${p.name}</h3><p>$${p.price}</p></div>`
})
container.innerHTML = html
}
}
Alice 的问题:每次状态变化都要手动清空容器、拼接 HTML,30 行代码只做了一件事——显示当前状态。
(2) React 的条件渲染解法
JSX
function ProductList({ user, products, isLoading, error }) {
// 每个状态独立成 if 分支,互不干扰
if (isLoading) return <LoadingSpinner />
if (error) return <ErrorMessage message={error} />
if (!user) return <LoginPrompt />
if (products.length === 0) return <EmptyState />
return (
<div className="product-grid">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}
收益:15 行代码覆盖全部状态,每个分支一目了然。
3. 条件渲染的 3 种方式
| 方式 | 语法 | 适用场景 | 注意事项 |
|---|---|---|---|
| 三元运算符 | condition ? A : B |
二选一(有 else 分支) | 嵌套三元可读性差,避免超过 2 层 |
逻辑与 && |
condition && A |
满足才显示(无 else) | 0 && <Comp /> 会渲染 0,用 > 0 |
| 逻辑或 ` | ` | `value |
(1) 三元运算符 ? :
适合"二选一"的场景:
JSX
function StatusBadge({ isActive }) {
return (
<span style={{ color: isActive ? '#52c41a' : '#ff4d4f' }}>
{isActive ? '🟢 在线' : '🔴 离线'}
</span>
)
}
// isActive=true → <span>🟢 在线</span>
// isActive=false → <span>🔴 离线</span>
(2) 逻辑与 &&
适合"满足条件才显示,否则不显示":
JSX
function Notification({ count }) {
return (
<div className="notification-icon">
🔔
{/* count > 0 时显示红点,否则什么都不显示 */}
{count > 0 && (
<span className="badge">{count > 99 ? '99+' : count}</span>
)}
</div>
)
}
// count=0 → 只显示 🔔
// count=5 → 显示 🔔 和 "5"
⚠️ 陷阱:不要写
{items.length && <List />}。当 items.length 为 0 时,0 && <List /> 的结果是 0,React 会把 0 渲染到页面上。正确写法是 {items.length > 0 && <List />}。
(3) 逻辑或 ||
适合"提供兜底值":
JSX
function UserProfile({ user }) {
return (
<div>
<h2>{user.nickname || '匿名用户'}</h2>
<p>{user.bio || '这个人很懒,什么都没写...'}</p>
<p>位置:{user.location || '未知'}</p>
</div>
)
}
▶ 示例:三种条件渲染同场对比
JSX
// ============================================
// 示例:用户面板中三种条件渲染的综合运用
// ============================================
function UserPanel({ user }) {
return (
<div className="panel">
{/* 1. 三元:二选一 */}
<h2>
{user.isVIP ? '⭐ 尊贵会员' : '普通用户'}
</h2>
{/* 2. &&:满足才显示 */}
{user.isVIP && (
<p>会员到期:{user.vipExpireDate}</p>
)}
{/* 3. ||:兜底值 */}
<p>签名:{user.slogan || '此人很酷,没有签名'}</p>
{/* 组合使用:标签列表 */}
{user.tags && user.tags.length > 0 && (
<div>
{user.tags.map(tag => (
<span key={tag} style={{ background: '#f0f0f0', padding: '2px 8px', margin: '2px', borderRadius: '4px' }}>
{tag}
</span>
))}
</div>
)}
</div>
)
}
4. 列表渲染:.map()
(1) 基础用法
Array.map() 遍历数组,为每个元素返回一个 JSX:
JSX
const fruits = ['苹果', '香蕉', '橙子', '葡萄']
function FruitList() {
return (
<ul>
{fruits.map(fruit => (
<li>{fruit}</li>
))}
</ul>
)
}
// 输出:
// <ul>
// <li>苹果</li>
// <li>香蕉</li>
// <li>橙子</li>
// <li>葡萄</li>
// </ul>
(2) 渲染对象数组
JSX
function ScoreTable() {
const students = [
{ id: 1, name: 'Alice', score: 92 },
{ id: 2, name: 'Bob', score: 85 },
{ id: 3, name: 'Charlie', score: 78 }
]
return (
<table border="1" cellPadding="8">
<thead>
<tr><th>姓名</th><th>分数</th><th>等级</th></tr>
</thead>
<tbody>
{students.map(s => (
<tr key={s.id}>
<td>{s.name}</td>
<td>{s.score}</td>
<td>{s.score >= 90 ? '优秀' : s.score >= 80 ? '良好' : '及格'}</td>
</tr>
))}
</tbody>
</table>
)
}
(3) 列表渲染的 3 种数据转换技巧
JSX
const items = [
{ id: 1, name: 'React 基础', price: 0 },
{ id: 2, name: 'Vue 入门', price: 0 },
{ id: 3, name: 'Node.js 实战', price: 99 }
]
// 1. 直接渲染
{items.map(item => <li key={item.id}>{item.name}</li>)}
// 2. 过滤后再渲染(免费课程)
{items
.filter(item => item.price === 0)
.map(item => <li key={item.id}>{item.name}(免费)</li>)
}
// 3. 提取渲染函数
function renderItem(item) {
return (
<li key={item.id}>
{item.name} - ${item.price}
</li>
)
}
{items.map(renderItem)}
5. key 的重要性
React 用 key 识别列表中的每个元素。key 稳定时,React 复用 DOM 节点;key 变化时,React 销毁重建。
graph LR
A[列表变化] --> B{key 变化?}
B -->|No, key 相同| C[复用 DOM 节点<br/>只更新变化内容]
B -->|Yes, key 变了| D[销毁旧节点<br/>创建新节点]
B -->|No, 没有 key| E[全部重新渲染]
| key 策略 | 性能 | 推荐 |
|---|---|---|
| 数据库唯一 ID | ✅ 最优 | ⭐⭐⭐ |
| 唯一字符串(uuid) | ✅ 最优 | ⭐⭐⭐ |
| 数组索引 index | ⚠️ 有风险(列表变化时状态错乱) | ⭐ |
| 随机数 | ❌ 极差(每次渲染都变) | ❌ |
| 不传 key | ❌ 性能差 | ❌ |
▶ 示例:有 key vs 无 key
JSX
// ============================================
// 示例:key 对输入框状态的影响
// ============================================
function KeyDemo() {
const [items, setItems] = React.useState([
{ id: 'a', text: '任务 A' },
{ id: 'b', text: '任务 B' },
{ id: 'c', text: '任务 C' }
])
function shuffle() {
setItems([...items].sort(() => Math.random() - 0.5))
}
return (
<div>
<button onClick={shuffle}>打乱顺序</button>
<p>✅ 有 key(id 稳定,输入框内容不乱):</p>
<ul>
{items.map(item => (
<li key={item.id}>
{item.text} <input placeholder="输入内容" />
</li>
))}
</ul>
<p>❌ 无 key(或 index 作为 key,输入框内容会错乱):</p>
<ul>
{items.map((item, index) => (
<li key={index}>
{item.text} <input placeholder="内容会错位" />
</li>
))}
</ul>
</div>
)
}
// 点击"打乱顺序"后,有 key 的列表输入框内容不乱
// 用 index 的列表,输入框内容和文字会错位
6. 空状态和加载状态
| 状态 | 条件 | UI 表现 | 处理方式 |
|---|---|---|---|
| 加载中 | loading === true |
Spinner / 骨架屏 | if (loading) return <Spinner /> |
| 加载失败 | error !== null |
错误信息 + 重试按钮 | if (error) return <ErrorView /> |
| 空数据 | data.length === 0 |
空状态插图 + 引导文案 | if (!data.length) return <Empty /> |
| 正常 | 以上都不是 | 渲染数据列表 | 默认 return 正常 UI |
真实项目中,数据加载有 3 种必须处理的状态:
JSX
// ============================================
// 示例:完整的列表组件(3 种状态处理)
// ============================================
function ArticleList() {
const [articles, setArticles] = React.useState([])
const [isLoading, setIsLoading] = React.useState(true)
const [error, setError] = React.useState(null)
// 模拟加载(实际是 API 调用)
React.useEffect(() => {
fetch('/api/articles')
.then(res => {
if (!res.ok) throw new Error('网络请求失败')
return res.json()
})
.then(data => {
setArticles(data)
setIsLoading(false)
})
.catch(err => {
setError(err.message)
setIsLoading(false)
})
}, [])
// 1. 加载中
if (isLoading) {
return (
<div style={{ textAlign: 'center', padding: '40px' }}>
<div className="spinner" />
<p>正在加载文章...</p>
</div>
)
}
// 2. 加载失败
if (error) {
return (
<div style={{ textAlign: 'center', padding: '40px', color: '#ff4d4f' }}>
<p>❌ 加载失败:{error}</p>
<button onClick={() => window.location.reload()}>重新加载</button>
</div>
)
}
// 3. 空数据
if (articles.length === 0) {
return (
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
<p>📭 暂无文章</p>
<p>还没有人发布文章,快来写第一篇吧!</p>
</div>
)
}
// 4. 正常渲染
return (
<div>
<h3>文章列表(共 {articles.length} 篇)</h3>
{articles.map(article => (
<ArticleCard key={article.id} article={article} />
))}
</div>
)
}
7. 完整示例:商品分类与搜索
JSX
// ============================================
// 完整示例:商品列表(条件渲染 + 列表渲染综合)
// 功能:按分类筛选、搜索、空结果提示
// ============================================
function ShopPage() {
const [products] = React.useState([
{ id: 1, name: '无线鼠标', category: '电子', price: 89, sales: 2000 },
{ id: 2, name: '机械键盘', category: '电子', price: 299, sales: 1500 },
{ id: 3, name: '笔记本', category: '办公', price: 15, sales: 5000 },
{ id: 4, name: '办公椅', category: '家具', price: 899, sales: 800 },
{ id: 5, name: '台灯', category: '家具', price: 129, sales: 3000 },
{ id: 6, name: '显示器', category: '电子', price: 1599, sales: 600 }
])
const [search, setSearch] = React.useState('')
const [category, setCategory] = React.useState('全部')
// 过滤逻辑
const filtered = products.filter(p => {
const matchSearch = p.name.includes(search)
const matchCategory = category === '全部' || p.category === category
return matchSearch && matchCategory
})
// 获取分类列表(去重)
const categories = ['全部', ...new Set(products.map(p => p.category))]
return (
<div style={{ maxWidth: '800px', margin: '0 auto' }}>
<h2>🛒 商品列表</h2>
{/* 搜索框 */}
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="搜索商品名称..."
style={{ width: '100%', padding: '8px', marginBottom: '12px', border: '1px solid #d9d9d9', borderRadius: '4px' }}
/>
{/* 分类按钮(列表渲染) */}
<div style={{ marginBottom: '12px' }}>
{categories.map(cat => (
<button
key={cat}
onClick={() => setCategory(cat)}
style={{
padding: '6px 16px', margin: '0 4px',
backgroundColor: category === cat ? '#1890ff' : '#f0f0f0',
color: category === cat ? 'white' : '#333',
border: 'none', borderRadius: '4px', cursor: 'pointer'
}}
>
{cat}
</button>
))}
</div>
{/* 结果统计 */}
{search || category !== '全部' ? (
<p style={{ color: '#666' }}>
{search && `搜索"${search}" `}
{category !== '全部' && `/${category} `}
共 {filtered.length} 件商品
</p>
) : null}
{/* 空结果 */}
{filtered.length === 0 ? (
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
<p>😕 没有找到匹配的商品</p>
<button
onClick={() => { setSearch(''); setCategory('全部') }}
style={{ padding: '8px 16px', marginTop: '8px', cursor: 'pointer' }}
>
清除全部筛选
</button>
</div>
) : (
/* 商品网格(列表渲染) */
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px' }}>
{filtered.map(product => (
<div key={product.id} style={{ border: '1px solid #eee', borderRadius: '8px', padding: '16px' }}>
<div style={{ height: '120px', backgroundColor: '#f5f5f5', borderRadius: '4px', marginBottom: '8px' }} />
<h4>{product.name}</h4>
<p style={{ color: '#ff4d4f', fontWeight: 'bold', fontSize: '18px' }}>${product.price}</p>
<p style={{ color: '#999', fontSize: '13px' }}>已售 {product.sales} 件</p>
<span style={{ background: '#f0f5ff', color: '#1890ff', padding: '2px 8px', borderRadius: '4px', fontSize: '12px' }}>
{product.category}
</span>
</div>
))}
</div>
)}
</div>
)
}
预期输出:完整的商品列表页,支持搜索过滤、分类筛选、空结果提示和统计信息。
▶ 示例 5:动态表单字段渲染
JSX
📖 仅展示
function DynamicForm() {
const [formType, setFormType] = useState('personal')
const [formData, setFormData] = useState({
name: '',
email: '',
company: '',
taxId: '',
budget: '',
})
function handleChange(e) {
const { name, value } = e.target
setFormData(prev => ({ ...prev, [name]: value }))
}
const personalFields = (
<>
<input name="name" value={formData.name} onChange={handleChange} placeholder="Name" />
<input name="email" value={formData.email} onChange={handleChange} placeholder="Email" />
</>
)
const businessFields = (
<>
<input name="company" value={formData.company} onChange={handleChange} placeholder="Company" />
<input name="taxId" value={formData.taxId} onChange={handleChange} placeholder="Tax ID" />
<input name="budget" value={formData.budget} onChange={handleChange} placeholder="Budget ($)" />
</>
)
return (
<div style={{ maxWidth: 400, margin: '0 auto' }}>
<h3>Registration</h3>
<div style={{ marginBottom: 12 }}>
{['personal', 'business'].map(type => (
<button
key={type}
onClick={() => setFormType(type)}
style={{
padding: '6px 16px', marginRight: 8, cursor: 'pointer',
backgroundColor: formType === type ? '#1890ff' : '#f0f0f0',
color: formType === type ? 'white' : '#333', border: 'none', borderRadius: 4,
}}
>
{type === 'personal' ? 'Personal' : 'Business'}
</button>
))}
</div>
{formType === 'personal' ? personalFields : businessFields}
<button style={{ marginTop: 12, padding: '8px 24px', cursor: 'pointer' }}>Submit</button>
</div>
)
}
▶ 示例 3:列表搜索过滤与排序
JSX
function FilterableList() {
const [items] = useState([
{ id: 1, name: 'React', category: 'Frontend', stars: 220 },
{ id: 2, name: 'Django', category: 'Backend', stars: 78 },
{ id: 3, name: 'Vue', category: 'Frontend', stars: 207 },
{ id: 4, name: 'Express', category: 'Backend', stars: 63 },
{ id: 5, name: 'Tailwind', category: 'CSS', stars: 80 },
])
const [search, setSearch] = useState('')
const [sortKey, setSortKey] = useState('name')
const filtered = items
.filter(i => i.name.toLowerCase().includes(search.toLowerCase()))
.sort((a, b) => sortKey === 'stars' ? b.stars - a.stars : a.name.localeCompare(b.name))
return (
<div style={{ maxWidth: 400, margin: '0 auto' }}>
<input value={search} onChange={e => setSearch(e.target.value)}
placeholder="Search frameworks..." style={{ width: '100%', padding: 8, borderRadius: 4, marginBottom: 8 }} />
<div style={{ marginBottom: 8 }}>
<button onClick={() => setSortKey('name')} style={{ padding: '4px 12px', marginRight: 4, cursor: 'pointer', background: sortKey === 'name' ? '#1890ff' : '#f0f0f0', color: sortKey === 'name' ? 'white' : '#333', border: 'none', borderRadius: 4 }}>A-Z</button>
<button onClick={() => setSortKey('stars')} style={{ padding: '4px 12px', cursor: 'pointer', background: sortKey === 'stars' ? '#1890ff' : '#f0f0f0', color: sortKey === 'stars' ? 'white' : '#333', border: 'none', borderRadius: 4 }}>Stars</button>
</div>
{filtered.length === 0 ? (
<p style={{ color: '#999', textAlign: 'center' }}>No results</p>
) : (
<ul style={{ listStyle: 'none', padding: 0 }}>
{filtered.map(i => <li key={i.id} style={{ padding: 8, borderBottom: '1px solid #f0f0f0', display: 'flex', justifyContent: 'space-between' }}><span>{i.name} <small style={{ color: '#999' }}>({i.category})</small></span><span>⭐ {i.stars}k</span></li>)}
</ul>
)}
</div>
)
}
▶ 示例 4:嵌套列表渲染——分类商品展示
JSX
function CategorizedProducts() {
const categories = [
{ id: 'electronics', name: 'Electronics', products: [
{ id: 1, name: 'Keyboard', price: 79 },
{ id: 2, name: 'Monitor', price: 399 },
]},
{ id: 'furniture', name: 'Furniture', products: [
{ id: 3, name: 'Desk', price: 249 },
{ id: 4, name: 'Chair', price: 189 },
]},
{ id: 'books', name: 'Books', products: [] },
]
return (
<div style={{ maxWidth: 500, margin: '0 auto' }}>
{categories.map(cat => (
<div key={cat.id} style={{ marginBottom: 16 }}>
<h3 style={{ borderBottom: '2px solid #1890ff', paddingBottom: 4 }}>{cat.name}</h3>
{cat.products.length === 0 ? (
<p style={{ color: '#999', fontStyle: 'italic', paddingLeft: 16 }}>No products yet</p>
) : (
<ul style={{ listStyle: 'none', padding: 0, paddingLeft: 16 }}>
{cat.products.map(p => (
<li key={p.id} style={{ padding: 4, display: 'flex', justifyContent: 'space-between' }}>
<span>{p.name}</span><span>${p.price}</span>
</li>
))}
</ul>
)}
</div>
))}
</div>
)
}
❓ 常见问题
Q 三元表达式和 && 写法哪个更好?
A 各有场景:
&& 适合"满足条件就显示,不满足就什么都不渲染";三元 ? : 适合"条件 A 显示组件 X,条件 B 显示组件 Y"。如果"不满足时什么都不显示"用三元写就是 condition ? <Comp /> : null,此时用 && 更简洁。但注意 && 的 0 渲染陷阱——0 && <Comp /> 会渲染出 0。📖 小节
- 条件渲染有三种方式:
? :(二选一)、&&(仅显示)、||(兜底值) - 列表渲染用
.map(),每个 JSX 元素必须绑定唯一的key - 真实项目必须处理 3 种状态:加载中、加载失败、空数据
- 警惕
{items.length && <List />}的 0 渲染陷阱 - key 用唯一稳定的 ID,不要用 index
📝 作业
- 基础题(难度⭐):创建一个
TemperatureDisplay组件,根据tempProps:>35°C 显示 🔥 红色,15-35°C 显示 ☀️ 黄色,<15°C 显示 ❄️ 蓝色。 - 进阶题(难度⭐⭐):创建一个
TodoFilter组件,接收todos(任务数组)和filter(全部/已完成/未完成)两个 Props,实现按状态过滤显示。 - 挑战题(难度⭐⭐⭐):创建一个
Pagination组件,接收totalPages、currentPage、onPageChangeProps,渲染页码按钮,支持首尾页、省略号。