你的 React 组件里一定有条件渲染。
{condition && <Component />}、三元表达式、if/else——这些你每天都在写。
但这些写法里有 5 种坑,不出问题的时候一切正常,一旦触发就是线上事故。我在生产环境里被炸了不止一次。
// ❌ 你以为它不会渲染,但它渲染了一个 "0"
function NotificationBadge({ count }) {
return (
<div>
{count && <span className="badge">{count}</span>}
</div>
);
}
// 当 count = 0 时,页面上会出现一个孤零零的 "0"
为什么? && 的短路规则:左侧是 falsy 就返回左侧的值。0 是 falsy,所以返回 0。React 会把数字 0 渲染到 DOM 里。
同样的坑还有 "" 空字符串——不会渲染,但 NaN 会渲染出 "NaN"。
// ✅ 显式转为布尔值
{count > 0 && <span className="badge">{count}</span>}
// 或者用三元
{count ? <span className="badge">{count}</span> : null}
// 通用安全写法:!!
{!!items.length && <List items={items} />}
原则:&& 左侧必须是布尔表达式,永远不要放数字或可能为 0 的变量。
// ❌ 三个月后你自己都看不懂
return (
<div>
{isLoading
? <Spinner />
: error
? <ErrorMessage error={error} />
: data
? data.items.length > 0
? <List items={data.items} />
: <EmptyState />
: null}
</div>
);
这段代码现在能跑。但下周产品说"加载失败要有重试按钮",你要在哪层加?哪层是 error 的分支?看 3 分钟才能确认。
// ✅ 早返回 + 分离
function DataView({ isLoading, error, data }) {
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
if (!data || data.items.length === 0) return <EmptyState />;
return <List items={data.items} />;
}
原则:三元表达式最多嵌套一层。超过一层就拆成 if 早返回或抽成子组件。
// ❌ 切换tab后表单输入全丢了
function TabPanel({ activeTab }) {
return (
<div>
{activeTab === 'form' && <UserForm />}
{activeTab === 'preview' && <Preview />}
</div>
);
}
用户在表单里填了一堆数据,切到 Preview 看看效果,切回来——表单空了。
因为 && 条件为 false 时组件被卸载了,所有内部 state 都丢了。
// ✅ 方案1:用 CSS 隐藏而不是卸载
function TabPanel({ activeTab }) {
return (
<div>
<div style={{ display: activeTab === 'form' ? 'block' : 'none' }}>
<UserForm />
</div>
<div style={{ display: activeTab === 'preview' ? 'block' : 'none' }}>
<Preview />
</div>
</div>
);
}
// ✅ 方案2:状态提升到父组件
function TabPanel({ activeTab }) {
const [formData, setFormData] = useState({});
return (
<div>
{activeTab === 'form' && (
<UserForm data={formData} onChange={setFormData} />
)}
{activeTab === 'preview' && <Preview data={formData} />}
</div>
);
}
原则:条件渲染 = 卸载/重新挂载。如果组件有用户输入或内部状态,要么用 CSS 隐藏,要么把状态提升。
// ❌ 用useEffect同步计算一个本该直接派生的值
function ProductList({ products, category }) {
const [filteredProducts, setFilteredProducts] = useState([]);
useEffect(() => {
setFilteredProducts(products.filter(p => p.category === category));
}, [products, category]);
return (
<div>
{filteredProducts.length > 0
? <List items={filteredProducts} />
: <EmptyState />}
</div>
);
}
看着好像没问题。但这里有两个 bug:
useState([])),然后 useEffect 执行后触发第二次渲染。用户会看到 EmptyState 闪一下。// ✅ 直接计算,不要"同步"
function ProductList({ products, category }) {
const filteredProducts = products.filter(p => p.category === category);
return (
<div>
{filteredProducts.length > 0
? <List items={filteredProducts} />
: <EmptyState />}
</div>
);
}
没有 state,没有 effect,没有闪烁,没有多余渲染。 如果 filter 逻辑很重,用 useMemo——但别用 useEffect + setState。
原则:能从 props/state 直接算出来的值,就直接算。不要用 useEffect "同步"到另一个 state。
(这个也是我之前写过的 useEffect 反模式之一——但在条件渲染场景中特别容易犯。)
// ❌ 两个输入框共享了同一个DOM,状态互相污染
function AuthForm({ mode }) {
return (
<div>
{mode === 'login'
? <input placeholder="用户名" />
: <input placeholder="邮箱" />}
</div>
);
}
用户在登录模式输入了用户名,切到注册模式——邮箱输入框里出现了刚才输入的用户名。
原因:React 的 reconciliation 认为这是同一个 <input> 节点(相同位置、相同类型),所以复用 DOM,不重置 state。
// ✅ 给不同分支加不同的 key,强制React重建
function AuthForm({ mode }) {
return (
<div>
{mode === 'login'
? <input key="login" placeholder="用户名" />
: <input key="register" placeholder="邮箱" />}
</div>
);
}
key 不同 → React 销毁旧节点、创建新节点 → state 不会串台。
// 复杂场景:整个表单组件切换
{mode === 'login'
? <LoginForm key="login" />
: <RegisterForm key="register" />}
原则:当条件渲染的两个分支是"不同的东西"但 DOM 结构相似时,必须加不同的 key 防止状态串台。
| 定时炸弹 | 触发条件 | 修复 |
|---|---|---|
| && 渲染 "0" | 左侧是数字/NaN | 改用 > 0 或 !!
|
| 三元嵌套 | 超过 2 层 | 拆成 if 早返回 |
| 状态丢失 | 条件 false=卸载 | CSS 隐藏或状态提升 |
| 派生条件用 effect | useEffect+setState | 直接计算/useMemo |
| key 串台 | 同位置同类型 DOM | 加不同 key |
这 5 个里面,我在生产环境踩过 3 颗。第 1 颗(&&渲染 0)和第 5 颗(key 串台)是线上 bug 排查了半小时才定位到的——因为本地测试数据永远不为 0,永远不切换模式。
条件渲染看着简单,但它直接关系到 React 的渲染机制、reconciliation 算法、组件生命周期。写错了不会报错,只会在某个边界条件下默默炸给用户看。
你踩过哪颗炸弹?评论区说说。