你的React组件里一定有条件渲染。
{condition && <Component />}、三元表达式、if/else------这些你每天都在写。
但这些写法里有5种坑,不出问题的时候一切正常,一旦触发就是线上事故。我在生产环境里被炸了不止一次。
炸弹1:&& 短路渲染出一个"0"
tsx
// ❌ 你以为它不会渲染,但它渲染了一个 "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"。
tsx
// ✅ 显式转为布尔值
{count > 0 && <span className="badge">{count}</span>}
// 或者用三元
{count ? <span className="badge">{count}</span> : null}
// 通用安全写法:!!
{!!items.length && <List items={items} />}
原则:&& 左侧必须是布尔表达式,永远不要放数字或可能为 0 的变量。
炸弹2:三元嵌套超过两层
tsx
// ❌ 三个月后你自己都看不懂
return (
<div>
{isLoading
? <Spinner />
: error
? <ErrorMessage error={error} />
: data
? data.items.length > 0
? <List items={data.items} />
: <EmptyState />
: null}
</div>
);
这段代码现在能跑。但下周产品说"加载失败要有重试按钮",你要在哪层加?哪层是 error 的分支?看3分钟才能确认。
tsx
// ✅ 早返回 + 分离
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 早返回或抽成子组件。
炸弹3:条件渲染导致组件状态丢失
tsx
// ❌ 切换tab后表单输入全丢了
function TabPanel({ activeTab }) {
return (
<div>
{activeTab === 'form' && <UserForm />}
{activeTab === 'preview' && <Preview />}
</div>
);
}
用户在表单里填了一堆数据,切到 Preview 看看效果,切回来------表单空了。
因为 && 条件为 false 时组件被卸载了,所有内部 state 都丢了。
tsx
// ✅ 方案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 隐藏,要么把状态提升。
炸弹4:用 useEffect + state 做"派生"条件
tsx
// ❌ 用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 闪一下。 - 不必要的渲染:每次 products/category 变化会触发两次渲染------一次正常渲染、一次 useEffect 触发的 setState。
tsx
// ✅ 直接计算,不要"同步"
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 反模式之一------但在条件渲染场景中特别容易犯。)
炸弹5:key 相同导致条件切换时状态"串台"
tsx
// ❌ 两个输入框共享了同一个DOM,状态互相污染
function AuthForm({ mode }) {
return (
<div>
{mode === 'login'
? <input placeholder="用户名" />
: <input placeholder="邮箱" />}
</div>
);
}
用户在登录模式输入了用户名,切到注册模式------邮箱输入框里出现了刚才输入的用户名。
原因:React 的 reconciliation 认为这是同一个 <input> 节点(相同位置、相同类型),所以复用 DOM,不重置 state。
tsx
// ✅ 给不同分支加不同的 key,强制React重建
function AuthForm({ mode }) {
return (
<div>
{mode === 'login'
? <input key="login" placeholder="用户名" />
: <input key="register" placeholder="邮箱" />}
</div>
);
}
key 不同 → React 销毁旧节点、创建新节点 → state 不会串台。
tsx
// 复杂场景:整个表单组件切换
{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算法、组件生命周期。写错了不会报错,只会在某个边界条件下默默炸给用户看。
你踩过哪颗炸弹?评论区说说。