React 条件渲染
从一个实际的 bug 说起
假设你正在开发一个购物车页面,需要在购物车有商品时显示商品列表,没有商品时显示空状态。你可能写出这样的代码:
function Cart({ items }) {
return (
<div>
<h2>购物车</h2>
{items.length && <CartList items={items} />}
</div>
);
}
看起来逻辑正确:如果 items.length 大于 0,就渲染 CartList。但实际运行时会发现,当购物车为空(items 是空数组 [])时,页面上渲染出的不是空状态提示,而是数字 0。
这个 bug 的根源在于对 React 条件渲染机制的误解。
React 条件渲染的三种基本写法
三元表达式:最直观的条件分支
三元表达式适合需要「二选一」的场景——要么渲染 A,要么渲染 B:
function UserStatus({ isLoggedIn }) {
return (
<div>
{isLoggedIn ? <LogoutButton /> : <LoginButton />}
</div>
);
}
三元表达式的优势是语义清晰:条件为真时渲染这个,为假时渲染那个,没有歧义。
当需要在 JSX 外部处理逻辑时,更推荐用早期 return(early return)模式:
function UserStatus({ isLoggedIn }) {
if (!isLoggedIn) {
return <LoginButton />;
}
return <LogoutButton />;
}
&& 运算符:适合「存在则渲染」的场景
&& 运算符适合「条件满足才渲染,条件不满足则什么都不渲染」的场景:
function Notification({ message }) {
return (
<div>
{message && <Alert>{message}</Alert>}
</div>
);
}
这个语义的本质是:如果条件为 truthy,就渲染右侧表达式;如果条件为 falsy,就不渲染。
if/else 早期返回:适合多条件分支
当有多个互斥的条件时,早期 return 模式比三元表达式嵌套更可读:
function OrderStatus({ status }) {
if (status === 'pending') {
return <Spinner />;
}
if (status === 'shipped') {
return <TrackingInfo />;
}
if (status === 'delivered') {
return <DeliveryConfirmation />;
}
return <UnknownStatus />;
}
&& 运算符的坑:为什么 0 && <div /> 渲染出 0
要理解这个行为,需要回顾 JavaScript 的逻辑短路求值规则。
&& 运算符在 JavaScript 中的求值规则是短路求值(short-circuit evaluation):从左到右求值,如果遇到第一个 falsy 值,就返回那个值,而不是继续求值。
true && 'hello' // 返回 'hello'
false && 'hello' // 返回 false
'foo' && 'bar' // 返回 'bar'
0 && 'hello' // 返回 0(不是 'hello'!)
'' && 'hello' // 返回 ''
null && 'hello' // 返回 null
React 在渲染 {expression} 时,会把 expression 的求值结果渲染到 DOM 中。对于 0 && <div />,React 先求值 0 && <div />,结果得到 0(因为 0 是 falsy,&& 运算符直接返回它),然后 React 将 0 作为文本节点渲染到 DOM。
所以当你在 JSX 中写 items.length && <CartList /> 时:
- 如果
items.length > 0(比如 3),3 && <CartList />求值结果是<CartList />(一个 React 元素),React 正常渲染 - 如果
items.length === 0,0 && <CartList />求值结果是0,React 渲染文本节点 "0"
正确的空状态处理
对于「列表为空」的场景,应该明确处理空状态,而不是依赖 && 的短路行为:
// 错误:空数组时渲染出 0
function Cart({ items }) {
return (
<div>
<h2>购物车</h2>
{items.length && <CartList items={items} />}
</div>
);
}
// 正确:明确处理空状态
function Cart({ items }) {
return (
<div>
<h2>购物车</h2>
{items.length > 0 ? (
<CartList items={items} />
) : (
<EmptyCart />
)}
</div>
);
}
// 或者用更语义化的写法
function Cart({ items }) {
const hasItems = items.length > 0;
return (
<div>
<h2>购物车</h2>
{hasItems ? <CartList items={items} /> : <EmptyCart />}
</div>
);
}
其他 falsy 值需要注意
除了 0,还有几个 JavaScript falsy 值会在 JSX 中被渲染出来:
// 这些都会在页面上渲染出可见内容,而不是"什么都不渲染"
{false && <div />} // 渲染 nothing(这是安全的,React 会跳过 false)
{0 && <div />} // 渲染 "0"
{'' && <div />} // 渲染 nothing(空字符串是 falsy)
{null && <div />} // 渲染 nothing
{undefined && <div />} // 渲染 nothing
false、null、undefined 在 JSX 中会被 React 跳过,不渲染任何内容。这是 React 设计的「友好」行为,但 0 和 '' 是例外,因为它们在 JavaScript 中虽然是 falsy,但却是有意义的值。
一个常见的模式是用 && 来过滤数组后渲染:
// 假设我们只显示未完成的任务
{activeTasks && activeTasks.map(task => <Task key={task.id} {...task} />)}
如果 activeTasks 是空数组 [],[] && ... 会返回右侧表达式(不是 false),这是正确的。但如果 activeTasks 恰好是 [0](数组里有一个数字 0),就会渲染出 0。不过这种边界情况非常罕见。
条件渲染和组件卸载的区别
条件渲染和组件卸载(unmount)是两个容易被混淆的概念。
条件渲染是指在特定条件满足时才渲染某个组件,条件不满足时完全不渲染该组件:
{isLoggedIn && <UserDashboard />}
当 isLoggedIn 变为 false 时,<UserDashboard /> 组件会被卸载(unmount),其 useEffect 的清理函数会运行,组件从 DOM 中移除。
但条件渲染不等于组件卸载——关键在于渲染树的深处发生了什么:
function Parent() {
const [showChild, setShowChild] = useState(false);
return (
<div>
<button onClick={() => setShowChild(false)}>Hide</button>
{showChild && <ExpensiveComponent />}
</div>
);
}
这里 ExpensiveComponent 卸载时会触发清理。但如果同一个组件只是隐藏(用 CSS display: none),组件并没有卸载:
function Parent() {
const [visible, setVisible] = useState(false);
return (
<div>
<button onClick={() => setVisible(false)}>Hide</button>
<ExpensiveComponent style={{ display: visible ? 'block' : 'none' }} />
</div>
);
}
这个例子里 ExpensiveComponent 永远不会卸载,useEffect 的清理函数不会运行。这意味着如果 ExpensiveComponent 内部有订阅逻辑,订阅不会被取消,可能导致内存泄漏。
工程判断:如果组件内部有副作用(订阅、定时器等),应该用条件渲染让组件在不需要时卸载,而不是用 CSS 隐藏。如果只是切换可见性且组件没有副作用,用 CSS 隐藏可能性能更好(因为不需要重新挂载)。
条件渲染 vs 早期 return:什么时候选哪个
早期 return(guard clause)模式是指在函数顶部提前返回,提前排除不符合条件的分支:
function UserProfile({ user }) {
if (!user) {
return <Loading />;
}
if (user.isBanned) {
return <BannedMessage />;
}
return (
<div>
<Avatar src={user.avatar} />
<Name name={user.name} />
<Bio bio={user.bio} />
</div>
);
}
条件渲染(inline 模式)是把条件内联在 JSX 中:
function UserProfile({ user }) {
return (
<div>
{!user ? (
<Loading />
) : user.isBanned ? (
<BannedMessage />
) : (
<>
<Avatar src={user.avatar} />
<Name name={user.name} />
<Bio bio={user.bio} />
</>
)}
</div>
);
}
两种模式的选择不是绝对的,但有一些工程上的考量:
用早期 return 的场景:
- 有多个互斥的终止条件(loading、error、空状态等)
- 每个分支的 JSX 结构差异很大
- 想在函数顶部就明确「什么情况下不继续」
用条件渲染的场景:
- 只有两个简单的分支
- 想保持 JSX 结构扁平,不想嵌套太深
- 条件是 UI 的一部分(比如复选框的选中状态)
一个常见的反模式是过度嵌套的三元表达式:
// 可读性差:不推荐
{isLoggedIn ? user.isAdmin ? <AdminPanel /> : <UserPanel /> : <LoginForm />}
这种嵌套三元应该用早期 return 改写。
多个条件分支的写法选择
当有多个条件时,有几种写法:
方案一:早期 return 链
function getStatusMessage(status) {
switch (status) {
case 'loading':
return <Spinner />;
case 'error':
return <ErrorMessage />;
case 'empty':
return <EmptyState />;
default:
return <Content />;
}
}
方案二:用对象映射替代 switch/if
const statusComponents = {
loading: <Spinner />,
error: <ErrorMessage />,
empty: <EmptyState />,
};
function Status({ status }) {
return statusComponents[status] || <Content />;
}
方案三:用 || 或 ?? 作为默认值的链式写法
function Status({ status }) {
return statusComponents[status] ?? <Content />;
}
对象映射方案的优势是可读性好、易于扩展。如果将来要增加新的状态,只需要修改 statusComponents 对象,不需要修改组件的逻辑结构。
但要注意,如果每个分支的渲染逻辑很复杂(有大量的 props 计算、条件 className 等),用对象映射会让代码变得难以阅读,此时早期 return 是更好的选择。
面试中的高频考点
条件渲染在面试中常被问到,主要考察对 React 渲染机制和 JavaScript 基础的理解深度。
考点一:&& 运算符的边界情况
面试官可能会问:「为什么 messages.length && <MessageList /> 在 messages 是空数组时会渲染出 0?」
这个问题考察的是:
- JavaScript 逻辑运算符的短路求值
- React 如何处理 JSX 中的表达式求值结果
- 0 是 falsy 但不是「什么都没有」
考点二:false、null、undefined 的区别
面试官可能会问:「{condition && <Component />} 中,condition 分别是 false、null、undefined 时,渲染结果是什么?」
正确答案是:
false:不渲染任何东西(React 跳过)null:不渲染任何东西(React 跳过)undefined:不渲染任何东西(React 跳过)0:渲染数字 0(这是很多人会忽略的)
考点三:条件渲染 vs CSS display:none
面试官可能会问:「组件内部有一个 useEffect 启动的定时器,如果用 CSS display:none 隐藏组件,定时器还在运行吗?」
答案是还在运行。因为 CSS 隐藏不会卸载组件,useEffect 的清理函数不会运行,定时器会继续计时。这可能导致内存泄漏和意外行为。
考点四:列表渲染中的假阳性
{items.filter(item => item.active).map(item => <Item key={item.id} {...item} />)}
如果所有 item 都是 inactive 的,filter 返回空数组 []。此时 [] && ... 返回右侧表达式(一个数组),React 会把这个空数组渲染为...什么都不渲染。但严格来说这是正确的行为——空数组没有元素,当然不需要渲染。
但如果原始数据中恰好有 0 这样的假值,可能会出问题:
items = [0, 1, 2]
{items.map(item => <div>{item}</div>)}
这会渲染出 0、1、2 三个文本节点,这不是 bug,但可能不是你想要的。
延展阅读
- React Docs: Conditional Rendering — 官方条件渲染文档
- React Beta Docs: You Might Not Need an Effect — 官方指南,讲解哪些逻辑不需要用 effect 处理
- Dan Abramov: JSX Conditions — 关于 JSX 条件渲染背后思考
- Kent C. Dodds: APIdocs - JSX — 深入分析 JSX 条件渲染的各种写法