React 条件渲染

深入理解 React 中三种条件渲染写法的机制差异,&& 运算符的 falsy 值陷阱,早期 return vs 条件渲染的工程选择,以及条件渲染在面试中的高频考点。

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 === 00 && <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

falsenullundefined 在 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?」

这个问题考察的是:

  1. JavaScript 逻辑运算符的短路求值
  2. React 如何处理 JSX 中的表达式求值结果
  3. 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>)}

这会渲染出 012 三个文本节点,这不是 bug,但可能不是你想要的。


延展阅读