目 录CONTENT

文章目录

React(七)条件渲染

React 19 条件渲染 —— 让页面"见机行事"的艺术


一、入门引导:为什么需要条件渲染?

1.1 生活中的"条件渲染"

想象你正在逛一个电商网站:

  • 商品库存充足时,显示**"立即购买"按钮;售罄时,按钮变成灰色的"已售罄"**。
  • 你是普通用户,只能看到**"个人中心";你是管理员,菜单栏会多出一个"后台管理"**入口。
  • 购物车为空时,页面显示**"去逛逛吧"的引导图;有商品时,展示商品列表和结算按钮**。

这些场景的本质都是同一个逻辑:根据某些条件的变化,动态决定显示什么、隐藏什么。这就是"条件渲染"。

1.2 React 19 条件渲染的核心定义

在 React 19 中,条件渲染是:

基于特定状态(state)、属性(props)或外部数据的逻辑判断结果,动态控制 UI 元素的挂载(mount)、渲染(render)、隐藏或销毁(unmount)的核心渲染机制。它是 React 声明式编程范式的典型落地场景。

用人话来说就是:"你只需要告诉 React'当条件 A 满足时,显示组件 X;否则显示组件 Y',React 会自动帮你处理所有的增删改查。" 你不需要手动操作 DOM,不需要写 document.getElementById().style.display = 'none' 这样的命令式代码——这就是声明式的魅力。

1.3 React 19 对条件渲染的原生优化

React 19 在条件渲染方面带来了几个关键优化:

优化点 说明
自动跳过不满足条件的组件预渲染开销 在遇到 falsenullundefined 等返回值时,直接跳过该节点的渲染流程,不会为其分配 fiber 节点的完整渲染资源
React Compiler(编译器优化) React 19 配套的编译器能自动分析条件分支中的依赖关系,从而生成更优的缓存策略,减少不必要的重渲染
Server Components 的条件渲染 在服务端组件中,条件渲染的结果可以直接在服务端确定,客户端只接收最终需要渲染的 HTML,大幅减少传输的 JS 体积

二、七种条件渲染实现方式(由浅入深)

2.1 方式一:if/else 语句 —— 最基础的组件级条件渲染

适用场景

  • 条件分支较多(3 个以上)
  • 每个分支的 JSX 结构差异较大
  • 逻辑需要在组件顶层清晰呈现

基础示例:根据用户登录状态显示不同内容

// ===== 完整可运行示例 =====
import React, { useState } from "react";

function UserGreeting() {
  // 状态:模拟用户是否已登录
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  // ---- 组件级的 if/else 条件渲染 ----
  // 在 return 之前用 if/else 决定要渲染的内容
  let content;
  if (isLoggedIn) {
    content = (
      <div>
        <h2>欢迎回来,张三!</h2>
        <button onClick={() => setIsLoggedIn(false)}>退出登录</button>
      </div>
    );
  } else {
    content = (
      <div>
        <h2>请先登录</h2>
        <button onClick={() => setIsLoggedIn(true)}>点击登录</button>
      </div>
    );
  }

  return (
    <div style={{ padding: "20px", border: "1px solid #ddd" }}>
      <h1>用户中心</h1>
      {content} {/* 将提前组装好的 JSX 插入 */}
    </div>
  );
}

export default function App() {
  return <UserGreeting />;
}

代码解读:

  1. useState(false) 创建一个初始值为 false 的状态变量 isLoggedIn,表示"未登录"
  2. return 语句之前,用标准的 JavaScript if/else 判断 isLoggedIn 的值
  3. 根据判断结果,将不同的 JSX 赋值给变量 content
  4. return 的 JSX 中,用花括号 {content} 将变量嵌入

进阶:在 JSX 中使用 if/else(元素级)

JSX 的 {} 中只能写表达式,不能写语句。但我们可以利用立即执行函数的技巧(详见 2.4 节):

// ❌ 错误写法 —— JSX 中不能直接写 if 语句
return (
  <div>
    {if (isLoggedIn) { <p>已登录</p> }}  {/* 这行代码会报错! */}
  </div>
);

// ✅ 正确写法 —— 把 if/else 放在 return 之前

课堂思考题 1:为什么 JSX 的花括号 {} 中不能写 if 语句,但可以写三元表达式?提示:想想 JavaScript 中"语句"和"表达式"的区别。


函数组件 vs 类组件中 if/else 的差异

函数组件(React 19 推荐写法):

function ProfilePage({ user }) {
  // 直接在函数体内用 if,清晰明了
  if (!user) {
    return <div>请先登录</div>; // 提前返回
  }

  // 后面的代码都是 "user 存在" 的情况
  return (
    <div>
      <h1>{user.name} 的个人主页</h1>
      <p>邮箱:{user.email}</p>
    </div>
  );
}

类组件(传统写法,了解即可):

class ProfilePage extends React.Component {
  render() {
    const { user } = this.props;

    // if 必须写在 render 方法内部
    if (!user) {
      return <div>请先登录</div>;
    }

    return (
      <div>
        <h1>{user.name} 的个人主页</h1>
        <p>邮箱:{user.email}</p>
      </div>
    );
  }
}

函数组件中的"提前返回"(early return)模式是条件渲染中非常常用的技巧,代码从上到下读起来就像"如果条件不满足就直接返回,否则继续往下执行",阅读体验极佳。

React 19 钩子中结合条件渲染的最佳实践

import { useState, useEffect, useMemo } from "react";

function Dashboard({ userId }) {
  const [userData, setUserData] = useState(null);

  // useEffect 中根据条件决定是否发请求
  useEffect(() => {
    if (!userId) return; // 条件不满足,直接跳过副作用

    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then((data) => setUserData(data));
  }, [userId]); // userId 变化时重新执行

  // useMemo 中条件性地缓存计算结果
  const permissions = useMemo(() => {
    if (!userData) return []; // 无数据 → 空权限
    if (userData.role === "admin") return ["read", "write", "delete"];
    if (userData.role === "editor") return ["read", "write"];
    return ["read"]; // 默认只读
  }, [userData]);

  // 渲染
  if (!userId) return <div>请选择用户</div>;
  if (!userData) return <div>加载中...</div>;

  return (
    <div>
      欢迎,{userData.name}(权限:{permissions.join("、")})
    </div>
  );
}

2.2 方式二:三元运算符 —— 最常用的行内条件渲染

适用场景

  • 简单的"二选一"场景(显示 A 或显示 B)
  • 需要在 JSX 中行内书写,保持代码紧凑
  • 适合按钮文字、类名、样式等简短内容的切换

基础示例:表单提交按钮的禁用状态

import React, { useState } from "react";

function LoginForm() {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [isSubmitting, setIsSubmitting] = useState(false);

  // 判断按钮是否应该禁用:任一输入为空 或 正在提交中
  const isButtonDisabled = !username || !password || isSubmitting;

  const handleSubmit = () => {
    setIsSubmitting(true);
    // 模拟 API 请求
    setTimeout(() => {
      alert(`登录成功!用户:${username}`);
      setIsSubmitting(false);
    }, 2000);
  };

  return (
    <div style={{ padding: 20 }}>
      <h2>用户登录</h2>
      <input
        placeholder="用户名"
        value={username}
        onChange={(e) => setUsername(e.target.value)}
      />
      <br />
      <br />
      <input
        type="password"
        placeholder="密码"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <br />
      <br />
      {/* ===== 三元运算符实现行内条件渲染 ===== */}
      <button
        disabled={isButtonDisabled}
        onClick={handleSubmit}
        style={{
          padding: "10px 30px",
          backgroundColor: isButtonDisabled ? "#ccc" : "#1890ff",
          color: isButtonDisabled ? "#999" : "#fff",
          border: "none",
          borderRadius: 4,
          cursor: isButtonDisabled ? "not-allowed" : "pointer",
        }}
      >
        {/* 按钮文字:根据状态动态切换 */}
        {isSubmitting ? "登录中..." : "立即登录"}
      </button>
      {/* ==================================== */}
    </div>
  );
}

export default function App() {
  return <LoginForm />;
}

代码解读:

  1. isButtonDisabled 综合了三个条件,决定按钮是否可点击
  2. {isSubmitting ? "登录中..." : "立即登录"} — 三元运算符的标准格式:条件 ? 真时的值 : 假时的值
  3. style 属性中也使用了多个三元表达式来动态切换样式
  4. 三元运算符可以直接写在 JSX 的 {} 中,因为它是表达式而非语句

三元嵌套规范

三元运算符可以嵌套,但有严格的规范:

// ✅ 正确:最多两层嵌套,格式清晰
function StatusBadge({ status }) {
  return (
    <span>
      {status === "online"
        ? "🟢 在线"
        : status === "away"
          ? "🟡 离开"
          : "🔴 离线"}
    </span>
  );
}

// ❌ 错误:三层及以上嵌套,可读性急剧下降
function BadExample({ type }) {
  return (
    <span>
      {type === "a" ? "A" : type === "b" ? "B" : type === "c" ? "C" : "D"}
    </span>
    // 这种代码应该改用 if/else 或 switch + IIFE
  );
}

// ✅ 更好的做法:超出两层的条件判断,抽离为独立逻辑
function StatusBadgeBetter({ status }) {
  const statusMap = {
    online: "🟢 在线",
    away: "🟡 离开",
    offline: "🔴 离线",
    busy: "⏰ 忙碌",
  };

  return <span>{statusMap[status] || "未知状态"}</span>;
}

常见语法错误:遗漏括号导致的优先级问题

// ❌ 错误:缺少括号包裹三元表达式
function WrongExample({ user }) {
  return (
    <div>
      用户名:{user ? user.name : "未登录" + " | 欢迎"}
      {/* 上面的代码实际上解析为:
          user ? user.name : ("未登录" + " | 欢迎")
          字符串拼接的优先级高于三元,但这容易引起混淆 */}
    </div>
  );
}

// ✅ 正确:用括号明确三元表达式的边界
function CorrectExample({ user }) {
  return (
    <div>
      用户名:{user ? user.name : "未登录"} | 欢迎
      {/* 括号明确了 "未登录" 是假分支的完整返回值 */}
    </div>
  );
}

// ✅ 更安全的写法:提取变量
function SafestExample({ user }) {
  const displayName = user ? user.name : "未登录";
  return <div>用户名:{displayName} | 欢迎</div>;
}

课堂思考题 2:以下代码的输出是什么?为什么?

const count = 0;
return <div>{count ? "有数据" : "无数据"}</div>;

2.3 方式三:&& 逻辑与运算符 —— 最简洁的"条件满足才渲染"

适用场景

  • 只需要"条件满足时显示,不满足时不渲染任何东西"
  • 如:错误提示、加载动画、特定标签的显示/隐藏
  • 代码要求极简,不需要 else 分支

基础示例:错误提示与空状态

import React, { useState } from "react";

function SearchPage() {
  const [keyword, setKeyword] = useState("");
  const [results, setResults] = useState(null);
  const [error, setError] = useState("");

  const handleSearch = () => {
    setError("");
    setResults(null);

    if (!keyword.trim()) {
      setError("请输入搜索关键词");
      return;
    }

    // 模拟搜索结果
    setTimeout(() => {
      if (keyword === "react") {
        setResults([
          { id: 1, title: "React 官方文档" },
          { id: 2, title: "React 入门教程" },
        ]);
      } else {
        setResults([]);
      }
    }, 800);
  };

  return (
    <div style={{ padding: 20 }}>
      <h2>搜索页面</h2>
      <input
        value={keyword}
        onChange={(e) => setKeyword(e.target.value)}
        placeholder="请输入搜索关键词"
        onKeyDown={(e) => e.key === "Enter" && handleSearch()}
      />
      <button onClick={handleSearch}>搜索</button>

      {/* ===== && 运算符的三种典型用法 ===== */}

      {/* 用法 1:错误提示 —— 有错误信息时才显示 */}
      {error && <p style={{ color: "red" }}>❌ {error}</p>}

      {/* 用法 2:加载状态 —— 请求进行中时显示 */}
      {results === null && error === "" && keyword && <p>搜索中...</p>}

      {/* 用法 3:空结果提示 —— 搜索完成但无结果时显示 */}
      {results !== null && results.length === 0 && (
        <p>暂无搜索结果,换个关键词试试吧</p>
      )}

      {/* 搜索结果列表 */}
      {results && results.length > 0 && (
        <ul>
          {results.map((item) => (
            <li key={item.id}>{item.title}</li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default function App() {
  return <SearchPage />;
}

🔥 重点:数字 0 渲染陷阱

这是 React 条件渲染中最经典的陷阱,React 19 中依然存在,面试必考!

function ZeroTrapDemo() {
  const [items, setItems] = useState([]);
  // items.length 初始值是 0

  return (
    <div>
      <h3>商品列表</h3>

      {/* ❌ 陷阱代码:当 items.length 为 0 时,页面会渲染出 "0" */}
      {items.length && <p>共 {items.length} 件商品</p>}

      {/* 原因:JavaScript 中 0 && JSX 的求值结果是 0(数字),
          React 19 会把数字 0 当作合法的渲染内容输出到页面上 */}
    </div>
  );
}

为什么会出现这个问题?

JavaScript 的 && 运算符的求值规则是:

  • 如果左侧是 falsy 值false0""nullundefinedNaN),直接返回左侧的值
  • 如果左侧是 truthy 值,返回右侧的值

所以 0 && <p>hello</p> 的求值结果是 0(数字类型)。React 会渲染数字 0,但不会渲染 falsenullundefined

// React 对各种值的渲染行为:
{
  false;
} // → 不渲染任何内容 ✅
{
  null;
} // → 不渲染任何内容 ✅
{
  undefined;
} // → 不渲染任何内容 ✅
{
  0;
} // → 渲染出 "0"        ⚠️ 陷阱!
{
  ("");
} // → 不渲染任何内容 ✅
{
  NaN;
} // → 渲染出 "NaN"      ⚠️ 注意!

三种解决方案

function ZeroTrapFixed() {
  const [items, setItems] = useState([]);

  return (
    <div>
      <h3>商品列表(方案对比)</h3>

      {/* ❌ 错误写法 */}
      {items.length && <p>共 {items.length} 件商品</p>}

      {/* ✅ 方案 1:显式转为布尔值 */}
      {items.length > 0 && <p>共 {items.length} 件商品</p>}

      {/* ✅ 方案 2:使用双重否定转为布尔值 */}
      {!!items.length && <p>共 {items.length} 件商品</p>}
      {/* !!0 → !false → true → false,所以不会渲染 */}

      {/* ✅ 方案 3:直接用三元运算符(最推荐) */}
      {items.length ? <p>共 {items.length} 件商品</p> : null}
    </div>
  );
}
方案 写法 优点 缺点
比较运算 items.length > 0 && 语义最清晰 稍微多几个字符
双重否定 !!items.length && 简短 初学者可能不理解 !! 的含义
三元运算符 items.length ? ... : null 意图明确,不会出错 多了 : null 部分

最佳实践建议:在团队项目中,统一使用 items.length > 0 && 或三元运算符,避免直接使用可能为数字类型的变量做 && 判断。

课堂思考题 3:以下代码页面会显示什么?

const arr = [];
return (
  <div>
    {arr.length && "有数据"}
    {!arr.length && "无数据"}
  </div>
);

2.4 方式四:IIFE(立即执行函数)—— 处理多分支复杂条件

适用场景

  • 3 个及以上的多分支条件判断(如:多种用户角色、多种订单状态)
  • 需要在 JSX 内部编写复杂逻辑,但不想抽成独立组件
  • switch/case 的分支逻辑

基础示例:订单状态的多种展示

import React, { useState } from "react";

function OrderCard({ status }) {
  // status 可能的取值: "pending" | "shipped" | "delivered" | "cancelled" | "refunded"

  return (
    <div style={{ padding: 20, border: "1px solid #eee" }}>
      <h3>订单详情</h3>

      {/* ===== IIFE 实现多分支条件渲染 ===== */}
      {(() => {
        // 在 IIFE 内部,可以使用任意 JavaScript 逻辑
        switch (status) {
          case "pending":
            return (
              <div style={{ color: "#faad14" }}>
                ⏳ 待发货
                <button>取消订单</button>
              </div>
            );
          case "shipped":
            return (
              <div style={{ color: "#1890ff" }}>
                🚚 已发货
                <button>确认收货</button>
              </div>
            );
          case "delivered":
            return (
              <div style={{ color: "#52c41a" }}>
                ✅ 已签收
                <button>去评价</button>
              </div>
            );
          case "cancelled":
            return <div style={{ color: "#999" }}>❌ 已取消</div>;
          case "refunded":
            return (
              <div style={{ color: "#ff4d4f" }}>
                💰 已退款
                <span>退款金额将退回原支付方式</span>
              </div>
            );
          default:
            return <div style={{ color: "#999" }}>未知状态</div>;
        }
      })()}
      {/* =================================== */}
    </div>
  );
}

function Demo() {
  const [currentStatus, setCurrentStatus] = useState("pending");

  const statuses = ["pending", "shipped", "delivered", "cancelled", "refunded"];

  return (
    <div>
      <div style={{ marginBottom: 10 }}>
        {statuses.map((s) => (
          <button
            key={s}
            onClick={() => setCurrentStatus(s)}
            style={{
              marginRight: 5,
              fontWeight: currentStatus === s ? "bold" : "normal",
            }}
          >
            {s}
          </button>
        ))}
      </div>
      <OrderCard status={currentStatus} />
    </div>
  );
}

export default function App() {
  return <Demo />;
}

代码解读:

  1. (() => { ... })() 是 IIFE(Immediately Invoked Function Expression)的语法
  2. 外层 () 包裹箭头函数,末尾 () 立即调用它
  3. 函数内部可以写任意 JavaScript 逻辑(switch、if、循环等)
  4. 函数 return 的值就是最终渲染的内容
  5. React 19 对 IIFE 内部返回的 JSX 会进行同样的渲染优化,不会因为多了一层函数调用就降低性能

更好的替代方案:将映射关系提取为对象

当条件判断是"值到 UI"的映射时,使用查找表(lookup table)比 IIFE 更优雅:

function OrderCardBetter({ status }) {
  // 将状态与 UI 的映射关系提取到组件外部
  const statusConfig = {
    pending: {
      icon: "⏳",
      text: "待发货",
      color: "#faad14",
      action: <button>取消订单</button>,
    },
    shipped: {
      icon: "🚚",
      text: "已发货",
      color: "#1890ff",
      action: <button>确认收货</button>,
    },
    delivered: {
      icon: "✅",
      text: "已签收",
      color: "#52c41a",
      action: <button>去评价</button>,
    },
  };

  const config = statusConfig[status];

  if (!config) return <div style={{ color: "#999" }}>未知状态</div>;

  return (
    <div style={{ color: config.color }}>
      {config.icon} {config.text}
      {config.action}
    </div>
  );
}

课堂思考题 4:什么情况下应该用 IIFE,什么情况下应该把逻辑提取成独立组件?各自的判断标准是什么?


2.5 方式五:高阶组件(HOC)—— 封装可复用的权限类条件渲染

适用场景

  • 多个组件需要相同的条件判断逻辑(如:权限校验、功能开关)
  • 需要在不修改原组件代码的情况下增强组件行为
  • 条件判断逻辑可以抽象为"包装器"

基础示例:withAuth 权限校验 HOC

import React, { useState, createContext, useContext } from "react";

// ---- 第一步:创建用户权限上下文 ----
const AuthContext = createContext();

// ---- 第二步:编写 withAuth 高阶组件 ----
// HOC 命名规范:以 with 开头
function withAuth(WrappedComponent, requiredPermission) {
  // 返回一个新组件,它"包裹"了原组件
  function WithAuthComponent(props) {
    // 从上下文中获取当前用户的权限列表
    const { permissions } = useContext(AuthContext);

    // 条件判断:用户是否有需要的权限
    if (!permissions.includes(requiredPermission)) {
      return (
        <div style={{ padding: 20, color: "#999", textAlign: "center" }}>
          🔒 您没有访问此功能的权限(需要:{requiredPermission})
        </div>
      );
    }

    // 有权限:正常渲染原组件,透传所有 props
    return <WrappedComponent {...props} />;
  }

  // ---- 修复 displayName,便于 React DevTools 调试 ----
  WithAuthComponent.displayName = `withAuth(${
    WrappedComponent.displayName || WrappedComponent.name
  })`;

  return WithAuthComponent;
}

// ---- 第三步:使用 HOC 保护组件 ----

// 管理员面板 —— 需要 "admin" 权限
function AdminPanel() {
  return (
    <div style={{ background: "#fff3cd", padding: 20 }}>
      <h3>管理员面板</h3>
      <button>用户管理</button>
      <button>系统设置</button>
      <button>数据导出</button>
    </div>
  );
}

// 编辑面板 —— 需要 "editor" 权限
function EditorPanel() {
  return (
    <div style={{ background: "#d1ecf1", padding: 20 }}>
      <h3>编辑面板</h3>
      <textarea placeholder="在此编辑内容..." rows={5} />
    </div>
  );
}

// 用 HOC 包装组件
const ProtectedAdminPanel = withAuth(AdminPanel, "admin");
const ProtectedEditorPanel = withAuth(EditorPanel, "editor");

// ---- 第四步:整体应用 ----
function AppContent() {
  const [role, setRole] = useState("visitor");

  const rolePermissions = {
    admin: ["admin", "editor", "viewer"],
    editor: ["editor", "viewer"],
    visitor: ["viewer"],
  };

  return (
    <AuthContext.Provider value={{ permissions: rolePermissions[role] }}>
      <div style={{ padding: 20 }}>
        <h2>权限系统演示</h2>
        <p>
          当前角色:
          <select value={role} onChange={(e) => setRole(e.target.value)}>
            <option value="admin">管理员</option>
            <option value="editor">编辑者</option>
            <option value="visitor">访客</option>
          </select>
        </p>
        <p>当前权限:{rolePermissions[role].join("、")}</p>

        <hr />
        <ProtectedAdminPanel />
        <br />
        <ProtectedEditorPanel />
      </div>
    </AuthContext.Provider>
  );
}

export default function App() {
  return <AppContent />;
}

代码解读:

  1. withAuth 是一个函数,它接收一个组件和一个权限名,返回一个新组件
  2. 新组件内部通过 useContext 获取当前用户的权限列表
  3. 如果权限不足,渲染"无权限"提示;权限满足,渲染原始组件
  4. displayName 的设置很重要!没有它,React DevTools 中只会显示 WithAuthComponent,无法区分不同的包装

HOC 的 displayName 问题

// ❌ 没有设置 displayName
function withAuth(WrappedComponent) {
  function WithAuthComponent(props) {
    /* ... */
  }
  return WithAuthComponent; // DevTools 中显示为 <WithAuthComponent>
}

// ✅ 设置了 displayName
function withAuth(WrappedComponent) {
  function WithAuthComponent(props) {
    /* ... */
  }
  WithAuthComponent.displayName = `withAuth(${WrappedComponent.displayName || WrappedComponent.name})`;
  return WithAuthComponent; // DevTools 中显示为 <withAuth(AdminPanel)>
}

课堂思考题 5:HOC 和"父组件包裹子组件"有什么本质区别?什么场景下你会选择 HOC 而不是直接在父组件中写条件判断?


2.6 方式六:自定义 Hook —— 封装可复用的条件逻辑

适用场景

  • 条件判断逻辑需要在多个组件中复用
  • 判断逻辑涉及状态管理副作用(如:API 请求、localStorage 读取)
  • 相比 HOC 更灵活,可以决定在组件的什么位置使用判断结果

示例 1:usePermission —— 权限判断 Hook

import React, { useState, useContext } from "react";

// ---- 自定义 Hook:usePermission ----
function usePermission(requiredPermission) {
  // 模拟从全局状态或 API 获取用户权限
  const [permissions] = useState(() => {
    // 实际项目中可能是从 Context 或 API 获取
    return ["read", "write"];
  });

  // 返回一个对象,包含判断结果和相关数据
  const hasPermission = permissions.includes(requiredPermission);

  return {
    hasPermission, // 是否有权限
    permissions, // 完整权限列表(按需暴露)
    deny: !hasPermission, // 便捷的反向判断
  };
}

// ---- 使用示例 ----
function DeleteButton() {
  const { hasPermission, deny } = usePermission("delete");

  return (
    <div>
      {hasPermission ? (
        <button style={{ background: "red", color: "white" }}>删除文档</button>
      ) : (
        <button disabled title="需要 delete 权限">
          删除文档(无权限)
        </button>
      )}
      {/* deny 属性的用法 —— 反向判断 */}
      {deny && (
        <span style={{ color: "#999", fontSize: 12, marginLeft: 8 }}>
          联系管理员获取删除权限
        </span>
      )}
    </div>
  );
}

function AdminTools() {
  const { hasPermission: canAdmin } = usePermission("admin");

  // Hook 返回多个属性,可以在不同位置使用
  return (
    <div>
      {canAdmin && (
        <div style={{ background: "#fff3cd", padding: 10 }}>
          <h4>管理工具</h4>
          <button>系统设置</button>
          <button>用户管理</button>
        </div>
      )}
    </div>
  );
}

export default function App() {
  return (
    <div style={{ padding: 20 }}>
      <h2>自定义 Hook 权限演示</h2>
      <DeleteButton />
      <br />
      <AdminTools />
    </div>
  );
}

示例 2:useFeatureFlag —— 功能开关 Hook

import React, { useState, useEffect, createContext, useContext } from "react";

// ---- 功能开关上下文(实际项目中可能从远程配置获取) ----
const FeatureFlagContext = createContext({});

function FeatureFlagProvider({ children }) {
  const [flags, setFlags] = useState({
    newDashboard: true, // 新版仪表盘(已开启)
    darkMode: false, // 暗黑模式(未开启)
    exportPDF: false, // PDF 导出(未开启)
  });

  // 模拟从远程配置中心加载功能开关
  useEffect(() => {
    // 实际项目中:fetch('/api/feature-flags').then(...)
    console.log("功能开关已加载");
  }, []);

  return (
    <FeatureFlagContext.Provider value={flags}>
      {children}
    </FeatureFlagContext.Provider>
  );
}

// ---- 自定义 Hook:useFeatureFlag ----
function useFeatureFlag(flagName) {
  const flags = useContext(FeatureFlagContext);

  // 返回开关状态 + 便捷的工具方法
  return {
    enabled: !!flags[flagName], // 是否开启
    disabled: !flags[flagName], // 是否关闭
  };
}

// ---- 使用示例 ----
function Dashboard() {
  const { enabled: showNewDashboard } = useFeatureFlag("newDashboard");

  return showNewDashboard ? (
    <div style={{ background: "#e6f7ff", padding: 20 }}>
      <h3>新版仪表盘</h3>
      <p>包含实时数据大屏、交互式图表等新功能</p>
    </div>
  ) : (
    <div style={{ background: "#f5f5f5", padding: 20 }}>
      <h3>经典仪表盘</h3>
      <p>基础数据展示</p>
    </div>
  );
}

function ExportTools() {
  const { enabled: canExportPDF } = useFeatureFlag("exportPDF");

  return (
    <div>
      {canExportPDF && <button>📄 导出 PDF</button>}
      <button>📊 导出 Excel</button>
      {!canExportPDF && canExportPDF === false && (
        <span style={{ color: "#999" }}> (PDF 导出功能尚未开放)</span>
      )}
    </div>
  );
}

export default function App() {
  return (
    <FeatureFlagProvider>
      <div style={{ padding: 20 }}>
        <h2>功能开关演示</h2>
        <Dashboard />
        <br />
        <ExportTools />
      </div>
    </FeatureFlagProvider>
  );
}

Hook vs HOC 的选择标准

维度 自定义 Hook 高阶组件(HOC)
使用方式 在组件内部调用 包裹组件
灵活性 高——可以决定在何处使用判断结果 低——整个组件被包裹
组合性 可以组合多个 Hook 多层 HOC 嵌套会造成"包装地狱"
Props 冲突 不会造成 props 冲突 需要注意 props 命名冲突
React 19 推荐度 ⭐⭐⭐⭐⭐ 官方推荐 ⭐⭐⭐ 可用,但更推荐 Hook

课堂思考题 6:为什么 React 19 官方更推荐用 Hook 替代 HOC?Hook 解决了 HOC 的哪些痛点?


2.7 方式七:关于 <Show> 组件

先澄清一个常见误解

React 19 并没有内置名为 <Show> 的条件渲染组件。<Show> 是 SolidJS 框架的原生组件,在 React 生态中如果需要类似功能,通常用第三方库(如 @react-show/show)或自行封装。

不过,我们可以自行封装一个类似的组件来简化条件渲染的写法:

import React from "react";

// ---- 自封装 Show 组件 ----
function Show({ when, fallback = null, children }) {
  // when 条件为真 → 渲染 children
  // when 条件为假 → 渲染 fallback(默认为 null,即不渲染)
  return when ? children : fallback;
}

// ---- 使用示例 ----
function Demo() {
  const [isEditing, setIsEditing] = React.useState(false);
  const [content, setContent] = React.useState("这是一段可编辑的内容");

  return (
    <div style={{ padding: 20 }}>
      <Show when={isEditing} fallback={<p>{content}</p>}>
        <div>
          <textarea
            value={content}
            onChange={(e) => setContent(e.target.value)}
            rows={4}
          />
          <br />
          <button onClick={() => setIsEditing(false)}>保存</button>
        </div>
      </Show>

      <button onClick={() => setIsEditing(!isEditing)}>
        {isEditing ? "取消编辑" : "编辑内容"}
      </button>
    </div>
  );
}

export default function App() {
  return <Demo />;
}

自封装 Show 组件的注意事项

// ⚠️ 注意:Show 作为组件,其 children 始终会被创建(JSX 的求值规则)
function PotentialIssue() {
  const [user, setUser] = React.useState(null);

  return (
    <Show when={!!user}>
      {/* 即使 user 为 null,这行 JSX 的 <div> 也会被创建!
          只不过不会被渲染到 DOM 中 */}
      <div>欢迎,{user.name}</div>
      {/* 如果 user 是 null,访问 user.name 会报错! */}
    </Show>
  );
}

// ✅ 解决方案:使用 render props 模式(函数作为 children)
function ShowSafe({ when, fallback = null, children }) {
  if (!when) return fallback;
  // children 是函数时才调用,避免提前求值
  return typeof children === "function" ? children() : children;
}

function FixedDemo() {
  const [user, setUser] = React.useState(null);

  return (
    <ShowSafe when={!!user} fallback={<p>请先登录</p>}>
      {/* 传入函数,只在条件满足时才执行 */}
      {() => <div>欢迎,{user.name}</div>}
    </ShowSafe>
  );
}

总结:在实际项目中,直接使用三元运算符或 && 比封装 <Show> 更简单直接。<Show> 适合团队已有统一约定、追求声明式写法的场景。


三、React 19 条件渲染的专属注意事项

3.1 组件卸载与状态重置

当条件从 true 变为 false 时,React 19 会完全卸载对应的组件,其内部的所有状态都会被销毁。当条件再次变为 true 时,组件会重新挂载,状态从头初始化。

import React, { useState } from "react";

// 计数器组件 —— 内部维护自己的计数状态
function Counter({ label }) {
  const [count, setCount] = useState(0);

  console.log(`${label} 渲染了,当前计数:${count}`);

  return (
    <div style={{ border: "1px solid #ddd", padding: 10, margin: 5 }}>
      <h4>{label}</h4>
      <p>计数:{count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

function Demo() {
  const [showCounter, setShowCounter] = useState(true);

  return (
    <div style={{ padding: 20 }}>
      <h3>组件卸载演示</h3>
      <button onClick={() => setShowCounter(!showCounter)}>
        {showCounter ? "隐藏" : "显示"}计数器
      </button>

      <p>
        当前 showCounter = <b>{String(showCounter)}</b>
      </p>

      {/* 条件渲染:计数器在隐藏/显示之间切换 */}
      {showCounter && <Counter label="组件A" />}
    </div>
  );
}

export default function App() {
  return <Demo />;
}

操作步骤:

  1. 点击"+1"将计数增加到 3
  2. 点击"隐藏计数器" → Counter 组件被卸载,状态销毁
  3. 再次点击"显示计数器" → Counter 重新挂载,计数从 0 开始

如果你希望保留隐藏组件的状态,有两种方案:

function KeepStateDemo() {
  const [showCounter, setShowCounter] = useState(true);

  return (
    <div>
      {/* 方案 1:使用 CSS display:none 替代条件卸载 */}
      <div style={{ display: showCounter ? "block" : "none" }}>
        <Counter label="方案1:display控制" />
      </div>

      {/* 方案 2:使用同一个 key 保持组件实例 */}
      {/* 注意:如果同位置切换不同的组件,key 相同则会复用 */}
      {showCounter ? (
        <Counter key="counter" label="方案2:key控制" />
      ) : (
        <div key="counter" style={{ padding: 10 }}>
          计数器已隐藏
        </div>
      )}
    </div>
  );
}
方案 优点 缺点
CSS display:none 状态完整保留,DOM 也在 DOM 节点仍存在,内存不释放
相同 key 状态保留,DOM 可复用 只适用于同一位置的组件切换
状态提升 状态由父组件管理 需要重构组件结构

3.2 key 属性的正确使用

当条件渲染导致同一位置切换不同的同级别组件时,React 19 用 key 来决定是复用还是重建组件实例。

import React, { useState, useRef, useEffect } from "react";

function InputWithLabel({ label, type }) {
  const inputRef = useRef(null);
  const [value, setValue] = useState("");

  useEffect(() => {
    console.log(`${label} 组件挂载`);
    return () => console.log(`${label} 组件卸载`);
  }, [label]);

  return (
    <div>
      <label>{label}:</label>
      <input
        ref={inputRef}
        type={type}
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder={`请输入${label}`}
      />
    </div>
  );
}

function TabSwitchDemo() {
  const [tab, setTab] = useState("login");

  return (
    <div style={{ padding: 20 }}>
      <h3>Tab 切换演示</h3>
      <div style={{ marginBottom: 10 }}>
        <button onClick={() => setTab("login")}>登录</button>
        <button onClick={() => setTab("register")}>注册</button>
      </div>

      {/* ===== 反例:没有 key,React 可能错误复用 ===== */}
      <h4>❌ 反例:无 key</h4>
      <div style={{ border: "1px solid red", padding: 10, marginBottom: 10 }}>
        {tab === "login" ? (
          <InputWithLabel label="邮箱" type="email" />
        ) : (
          <InputWithLabel label="手机号" type="tel" />
        )}
        {/* 问题:切换 tab 时,由于两个 input 在 DOM 中的位置相同,
            React 可能会复用同一个 DOM 节点和组件实例,
            导致输入框中的文字被保留,但 label 和 type 改变 */}
      </div>

      {/* ===== 正例:添加 key,确保切换时重建组件 ===== */}
      <h4>✅ 正例:有 key</h4>
      <div style={{ border: "1px solid green", padding: 10 }}>
        {tab === "login" ? (
          <InputWithLabel key="login" label="邮箱" type="email" />
        ) : (
          <InputWithLabel key="register" label="手机号" type="tel" />
        )}
        {/* 不同的 key 告诉 React:
            "这是一个完全不同的组件,请卸载旧的,创建新的" */}
      </div>
    </div>
  );
}

export default function App() {
  return <TabSwitchDemo />;
}

React 19 的组件复用判断规则:

  • 位置相同 + key 相同 → 复用组件实例(只更新 props)
  • 位置相同 + key 不同 → 销毁旧组件,创建新组件
  • 位置不同 → 按 key 匹配,找不到就创建新的

经验法则:当条件渲染在同位置切换不同类型的组件需要重置状态的同类组件时,记得添加不同的 key


3.3 服务端渲染(SSR)与 Server Components 中的注意事项

React 19 中,Server Components(服务端组件)和 Client Components(客户端组件)在条件渲染上有不同的限制:

// ===== Server Component(.server.jsx 或在 App Router 中默认是服务端组件)=====

// ✅ 可以在服务端组件中做条件渲染,逻辑在服务端执行
async function ServerProductPage({ productId }) {
  const product = await db.product.findUnique({ where: { id: productId } });

  // 服务端的条件渲染:没有找到商品 → 返回 404
  if (!product) {
    return <NotFound />;
  }

  return (
    <div>
      <h1>{product.name}</h1>
      {/* 条件渲染:根据库存显示不同状态 */}
      {product.stock > 0 ? (
        <AddToCartButton productId={product.id} />  {/* 客户端组件 */}
      ) : (
        <span>已售罄</span>
      )}
    </div>
  );
}

// ⚠️ 注意:Server Component 中不能使用 useState、useEffect 等客户端 Hook
// 不能使用 onClick、onChange 等事件处理器
// ===== Client Component('use client' 指令标记)=====

"use client";

import { useState, useEffect } from "react";

function ClientDashboard() {
  const [theme, setTheme] = useState("light");

  // ✅ 客户端组件的条件渲染:可以使用所有 React 特性
  useEffect(() => {
    const savedTheme = localStorage.getItem("theme");
    if (savedTheme) setTheme(savedTheme);
  }, []);

  return (
    <div
      className={
        theme === "dark" ? "bg-black text-white" : "bg-white text-black"
      }
    >
      {/* 客户端的交互式条件渲染 */}
      <button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
        切换主题
      </button>
    </div>
  );
}

避免水合不匹配(Hydration Mismatch)错误:

"use client";

function HydrationSafe() {
  // ❌ 危险:服务端和客户端可能产生不同的渲染结果
  function DangerousComponent() {
    // 服务端没有 window 对象,这里会出问题
    const isMobile = typeof window !== "undefined" && window.innerWidth < 768;

    return isMobile ? <MobileLayout /> : <DesktopLayout />;
    // 水合时可能会报错:
    // "Text content does not match server-rendered HTML"
  }

  // ✅ 安全做法:使用 useEffect + useState 延迟判断
  function SafeComponent() {
    const [isMobile, setIsMobile] = useState(false);

    useEffect(() => {
      setIsMobile(window.innerWidth < 768);
    }, []);

    return isMobile ? <MobileLayout /> : <DesktopLayout />;
  }

  return <SafeComponent />;
}

3.4 性能优化要点

3.4.1 useMemo 缓存条件渲染的产物

当条件渲染的 JSX 包含复杂的计算大量的子组件,且条件判断依赖的值变化不频繁时,使用 useMemo 避免不必要的重建:

import React, { useState, useMemo } from "react";

function ExpensiveList({ items, filter }) {
  // ❌ 每次渲染都重新执行 filter 和 map
  // const filteredItems = items.filter(item => item.category === filter);

  // ✅ 只在 items 或 filter 变化时才重新计算
  const filteredItems = useMemo(
    () => items.filter((item) => item.category === filter),
    [items, filter],
  );

  // ✅ 将条件渲染的整个 JSX 分支缓存
  const listContent = useMemo(() => {
    if (filteredItems.length === 0) {
      return <p>当前分类下暂无商品</p>;
    }

    return (
      <ul>
        {filteredItems.map((item) => (
          <li key={item.id}>
            {item.name} - ¥{item.price}
          </li>
        ))}
      </ul>
    );
  }, [filteredItems]); // 只在筛选结果变化时重新生成 JSX

  return (
    <div>
      <h3>商品列表({filteredItems.length} 件)</h3>
      {listContent}
    </div>
  );
}

3.4.2 React Compiler(React 19 的编译器优化)

React 19 引入的新编译器(React Compiler)能自动分析哪些值是稳定不变的,哪些可能变化,然后自动插入类似 useMemo 的缓存逻辑。这意味着:

// React Compiler 能自动识别出 greetAdmin 只依赖 role
// 并自动进行缓存优化,不需要手动写 useMemo
function Greeting({ user }) {
  // 编译器会自动分析并优化这个条件渲染
  return (
    <div>
      {user.role === "admin" ? (
        <h1>欢迎回来,管理员 {user.name}</h1>
      ) : (
        <h1>你好,{user.name}</h1>
      )}
    </div>
  );
}

最佳实践:升级到 React 19 并启用 React Compiler 后,大部分情况下不需要手动写 useMemouseCallback。但对于复杂的条件渲染产物(如包含大量子组件的情况),手动 useMemo 仍然是有价值的。

3.4.3 避免条件渲染导致的不必要重渲染

// ❌ 问题:每次父组件渲染,都会创建新的 <ExpensiveChild>
// 即使条件判断的结果没变
function ParentProblem({ user }) {
  return (
    <div>
      <h1>欢迎</h1>
      {user.isVip && <ExpensiveChild data={user.preferences} />}
      {/* 每次渲染时 React 都要重新创建 ExpensiveChild 的 JSX 描述对象 */}
    </div>
  );
}

// ✅ 优化思路 1:将条件渲染的部分抽取为独立组件
function ParentOptimized({ user }) {
  return (
    <div>
      <h1>欢迎</h1>
      <VipSection user={user} />
    </div>
  );
}

// 使用 React.memo 避免不必要的重渲染
const VipSection = React.memo(function VipSection({ user }) {
  if (!user.isVip) return null; // 非 VIP,不渲染任何内容

  return <ExpensiveChild data={user.preferences} />;
});

3.5 TypeScript 类型安全

在 TypeScript 项目中使用条件渲染时,注意处理各种类型情况:

import React, { useState } from "react";

// ---- 定义用户类型 ----
interface User {
  id: number;
  name: string;
  role: "admin" | "editor" | "viewer";
  avatar?: string; // 可选属性
  lastLogin?: Date; // 可选属性
}

// ---- 为条件渲染的 props 添加类型约束 ----
interface DashboardProps {
  user: User | null; // user 可能为 null(未登录)
}

function Dashboard({ user }: DashboardProps) {
  // ✅ TypeScript 会强制要求处理 user 为 null 的情况
  if (!user) {
    return <div>请先登录</div>;
  }

  // 在这里 TypeScript 知道 user 一定不是 null(类型收窄)
  return (
    <div>
      <h2>欢迎,{user.name}</h2>

      {/* ✅ 条件渲染可选属性:TypeScript 强制检查 */}
      {user.avatar ? (
        <img src={user.avatar} alt={user.name} />
      ) : (
        <div className="default-avatar">{user.name[0].toUpperCase()}</div>
      )}

      {/* ✅ 条件渲染角色相关内容 */}
      {user.role === "admin" ? (
        <AdminTools />
      ) : user.role === "editor" ? (
        <EditorTools />
      ) : (
        <ViewOnlyTools />
      )}
    </div>
  );
}

// ---- 泛型约束条件渲染 Hook ----
function useConditionalRender<T>(value: T | null | undefined): {
  exists: boolean;
  value: T;
} {
  return {
    exists: value !== null && value !== undefined,
    value: value as T,
  };
}

// ---- 使用示例 ----
function UserProfile({ user }: { user: User | null }) {
  const { exists, value: safeUser } = useConditionalRender(user);

  return (
    <div>
      {exists ? (
        <div>
          <h3>{safeUser.name}</h3>
          <p>角色:{safeUser.role}</p>
        </div>
      ) : (
        <p>用户不存在</p>
      )}
    </div>
  );
}

3.6 常见陷阱规避

陷阱 1:&& 数字 0 渲染陷阱(已详述于 2.3 节)

小结:变量值可能为数字 0 时,不要直接用 &&,改用 > 0!! 或三元运算符。

陷阱 2:空值渲染问题

function NullTrap() {
  const [data, setData] = useState(null);
  // data 初始值为 null

  return (
    <div>
      {/* ❌ 错误:data 为 null 时,访问 data.items 会报错 */}
      {data.items.length > 0 && <List items={data.items} />}
      {/* TypeError: Cannot read properties of null */}

      {/* ✅ 正确:先判断 data 是否存在 */}
      {data && data.items.length > 0 && <List items={data.items} />}

      {/* ✅ 更好:使用可选链(Optional Chaining) */}
      {(data?.items?.length ?? 0) > 0 && <List items={data.items} />}
    </div>
  );
}

陷阱 3:异步条件判断的时序问题

function AsyncTrap() {
  const [userId, setUserId] = useState(1);
  const [user, setUser] = useState(null);

  useEffect(() => {
    // 模拟 API 请求
    let cancelled = false;

    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then((data) => {
        if (!cancelled) setUser(data);
      });

    return () => {
      cancelled = true;
    };
  }, [userId]);

  return (
    <div>
      {/* ⚠️ 时序问题:当 userId 快速切换时,可能出现"新 userId 但旧 user 数据"的情况 */}
      {/* 比如:先请求 userId=1,再快速切换到 userId=2 */}
      {/* 如果请求 2 先返回,请求 1 后返回,最终会显示 userId=2 但 username 来自 id=1 */}

      {user && <p>用户名:{user.name}</p>}

      {/* ✅ 补充校验:确保返回的用户 ID 与当前请求的 ID 一致 */}
      {user && user.id === userId ? (
        <p>用户名:{user.name}</p>
      ) : (
        <p>加载中...</p>
      )}
    </div>
  );
}

陷阱 4:条件渲染中引用 DOM 节点的空指针问题

import React, { useState, useRef, useEffect } from "react";

function RefTrap() {
  const [showInput, setShowInput] = useState(false);
  const inputRef = useRef(null);

  useEffect(() => {
    // ❌ 危险:showInput 为 false 时,inputRef.current 是 null
    // inputRef.current.focus();  // TypeError: Cannot read properties of null

    // ✅ 安全做法:先判断 ref 是否存在
    if (inputRef.current) {
      inputRef.current.focus();
    }
  }, [showInput]);

  return (
    <div>
      <button onClick={() => setShowInput(!showInput)}>
        {showInput ? "隐藏" : "显示"}输入框
      </button>

      {showInput && <input ref={inputRef} placeholder="我会自动获得焦点" />}
    </div>
  );
}

四、课堂实操练习题

基础题:登录态差异化首页

题目描述:
实现一个简单的首页,包含以下功能:

  1. 页面顶部有一个登录/退出按钮
  2. 未登录时:显示"请登录后查看更多内容"的提示,以及一个模拟登录按钮
  3. 已登录时:显示"欢迎回来,用户名!"的问候语,以及一个退出登录按钮
  4. 页面的背景色和欢迎语的文字颜色根据登录状态变化

要求:

  • 使用 useState 管理登录状态
  • 使用三元运算符实现条件渲染
  • 使用一个完整的组件实现,不拆分多个文件

提示代码结构:

function HomePage() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  const [username] = useState("小明");

  // 你的代码...
}

预期效果:

  • 页面初始化显示"未登录"状态
  • 点击"登录"按钮后,显示欢迎语,样式改变
  • 点击"退出"按钮后,恢复"未登录"状态

进阶题:三种用户角色的权限菜单渲染

题目描述:
实现一个后台管理系统的侧边菜单,根据用户角色动态显示不同的菜单项:

角色 可见菜单项
admin(管理员) 首页、数据概览、用户管理、系统设置、操作日志
editor(编辑者) 首页、数据概览、内容管理
viewer(访客) 首页、数据概览(只读)

要求:

  1. 使用一个下拉选择框或按钮组切换当前用户角色
  2. 使用查找表(lookup table)+ IIFE 或提前返回的方式实现多分支条件渲染
  3. 每个菜单项显示对应的图标(文字图标即可,如 🏠)
  4. 当前角色不能访问的菜单项显示为灰色且不可点击
  5. 封装一个 useRole 自定义 Hook 来管理角色状态

挑战题:封装通用条件渲染组件(带动画过渡)

题目描述:
封装一个名为 <AnimatedShow> 的通用条件渲染组件,要求在条件切换时支持淡入淡出动画

功能要求:

  1. 接收 when 属性控制显示/隐藏
  2. 显示时:元素从透明(opacity: 0)渐变到不透明(opacity: 1),持续 300ms
  3. 隐藏时:元素从不透明渐变到透明,动画结束后真正卸载组件(释放内存)
  4. 支持配置动画时长(duration 属性,默认 300ms)
  5. 支持配置动画类型(animation 属性,至少支持 "fade""slide" 两种)

技术提示:

  • 需要同时使用 3 个状态:shouldRender(是否挂载 DOM)、isVisible(是否可见)、动画过渡状态
  • 隐藏流程:先将 isVisible 设为 false → 等待动画结束 → 再将 shouldRender 设为 false
  • 显示流程:先将 shouldRender 设为 true → 下一帧将 isVisible 设为 true

使用示例:

function Demo() {
  const [show, setShow] = useState(true);

  return (
    <div>
      <button onClick={() => setShow(!show)}>切换</button>
      <AnimatedShow when={show} duration={300} animation="fade">
        <div style={{ padding: 40, background: "#e6f7ff" }}>
          我会带着动画出现和消失!
        </div>
      </AnimatedShow>
    </div>
  );
}

五、面试题汇总

Q1:React 中 && 条件渲染时,为什么有时页面会出现 "0"?

标准回答:

JavaScript 的 && 运算符返回第一个 falsy 值或最后一个 truthy 值。当左侧变量的值为 0 时,0 && <Component /> 的求值结果为 0(数字类型)。React 的渲染规则中,falsenullundefined 不会渲染任何内容,但数字 0NaN 会被渲染到页面上。

解决方案:

  1. 使用显式比较:arr.length > 0 && <Component />
  2. 转为布尔值:!!arr.length && <Component />
  3. 使用三元运算符:arr.length ? <Component /> : null

Q2:条件渲染时组件会被卸载还是隐藏?如何保留状态?

标准回答:

React 的条件渲染({condition && <Component />})在条件变为 false 时会完全卸载组件,内部状态会被销毁。如果需要保留状态,有三种方案:

  1. 使用 CSS display:none 替代条件卸载(DOM 节点保留,状态保留)
  2. 使用相同的 key 属性维护组件实例
  3. 将状态提升到父组件管理

Q3:React 19 中,为什么条件渲染时建议添加 key 属性?

标准回答:

当在同一个位置根据条件切换不同类型的组件时,React 通过 key 属性来识别组件是否应该被复用。如果两个组件有相同的 key,React 认为它们是"同一个"组件,会保留组件实例、只更新 props。如果 key 不同,React 会销毁旧组件、创建新组件。不添加 key 时,React 可能错误地复用了不应该复用的组件,导致状态混乱或 DOM 更新异常。


Q4:三元运算符最多可以嵌套几层?为什么?

标准回答:

建议最多嵌套两层。超过两层的三元嵌套会严重降低代码可读性,让其他开发者难以理解逻辑。当需要三层及以上的条件判断时,应该改用:

  1. if/else 语句提前返回
  2. IIFE 配合 switch/case
  3. 查找表(lookup table)
  4. 抽取为独立组件

Q5:HOC 和自定义 Hook 在封装条件渲染逻辑时如何选择?

标准回答:

场景 推荐方案 原因
整个组件的显示/隐藏由条件控制 HOC 逻辑干净地包裹在组件外部
组件内部多处使用条件判断结果 自定义 Hook 灵活,可以在组件的不同位置使用
需要组合多个条件判断 自定义 Hook 多个 Hook 可以自由组合
React 19 新项目 自定义 Hook React 19 官方推荐的复用模式

总体来说,现代 React 更推荐使用自定义 Hook,因为它没有 HOC 的包装地狱问题和 props 命名冲突问题。


Q6:Server Components 中能做条件渲染吗?有什么限制?

标准回答:

可以,但有严格的限制:

  • 可以做:基于数据库查询结果的条件渲染、基于 URL 参数的条件渲染
  • 不能做:使用 useState/useEffect 等客户端 Hook 的条件渲染、基于浏览器 API(如 window.innerWidth)的条件渲染
  • 条件渲染的结果会在服务端确定,客户端只接收最终的 HTML,需要注意不要在服务端和客户端渲染出不一致的内容(水合不匹配问题)

Q7:useMemo 在条件渲染中什么时候该用?什么时候不该用?

标准回答:

该用 useMemo 的场景:

  • 条件渲染的 JSX 包含大量子组件或复杂的列表渲染
  • 条件判断依赖的值变化不频繁
  • 条件分支的计算本身消耗较大(如:对大量数据进行 filter)

不该用 useMemo 的场景:

  • 简单的文本或单个元素的条件渲染(useMemo 本身的缓存开销可能大于渲染开销)
  • React 19 编译器已启用(编译器会自动优化大部分场景)
  • 条件判断依赖的值变化非常频繁(缓存命中率低,反而浪费内存)

六、总结

条件渲染是 React 开发中最基础也最高频使用的技能之一。掌握它,意味着你可以轻松应对 80% 以上的 UI 交互需求。

七种实现方式速查表:

方式 适用场景 复杂度
if/else(组件级) 多分支、差异大的结构
三元运算符 简单的二选一
&& 条件满足才显示
IIFE + switch 多分支、需要行内 ⭐⭐
高阶组件(HOC) 横切关注点复用 ⭐⭐⭐
自定义 Hook 灵活的条件逻辑复用 ⭐⭐
查找表(lookup table) 值到 UI 的映射 ⭐⭐

React 19 核心关注点:

  • 利用 React Compiler 避免过度手动优化
  • Server Components 中注意条件渲染的边界
  • 始终警惕 && 的数字 0 陷阱
  • 条件切换时注意 key 属性的正确使用

一句话总结:条件渲染就是"让 React 替你操心 DOM 的增删改查,你只管描述什么时候显示什么"—— 这就是声明式编程的核心魅力。

0

评论区