在这里插入图片描述

条件渲染

一、什么是条件渲染?

条件渲染是指根据不同的条件(如状态、属性、用户输入等)来决定渲染哪些 UI 元素。这是 React 中最常见的模式之一。

核心概念

  • 使用 JavaScript 条件语句(ifswitch)或运算符(&&? :)来决定渲染内容
  • React 的声明式特性使得条件渲染非常直观
  • 可以实现:登录/退出按钮、权限控制、加载状态、表单验证提示等

二、条件渲染的 5 种方式

2.1 if/else 语句

最适合逻辑复杂、代码块较长的场景。

function UserGreeting({ isLoggedIn, user }) {
  if (isLoggedIn && user) {
    return (
      <div className="welcome">
        <h1>欢迎回来,{user.name}!</h1>
        <p>上次登录:{user.lastLogin}</p>
      </div>
    );
  } else if (isLoggedIn) {
    return <h1>欢迎回来!</h1>;
  } else {
    return (
      <div className="login-prompt">
        <h1>请先登录</h1>
        <button>去登录</button>
      </div>
    );
  }
}

2.2 三元运算符 (condition ? expr1 : expr2)

最适合简单的二分支场景,简洁直观。

function StatusBadge({ status }) {
  return (
    <span className={`badge ${status === 'active' ? 'badge-success' : 'badge-danger'}`}>
      {status === 'active' ? '在线' : '离线'}
    </span>
  );
}

// 嵌套使用(谨慎,可读性会下降)
function ComplexStatus({ status }) {
  return (
    <div>
      {status === 'loading' ? (
        <Spinner />
      ) : status === 'error' ? (
        <ErrorMessage />
      ) : (
        <SuccessMessage />
      )}
    </div>
  );
}

2.3 逻辑与运算符 (&&)

最适合"满足条件才渲染,否则什么都不渲染"的场景。

function NotificationBadge({ count }) {
  return (
    <div className="notification">
      消息
      {count > 0 && <span className="badge">{count}</span>}
    </div>
  );
}

function AdminPanel({ user }) {
  return (
    <div>
      <h1>仪表盘</h1>
      {user.isAdmin && <AdminTools />}
      {user.canDelete && <DeleteButton />}
      {user.canEdit && <EditButton />}
    </div>
  );
}

2.4 switch 语句

最适合多分支、每种分支逻辑较复杂的场景。

function OrderStatus({ status }) {
  switch (status) {
    case 'pending':
      return (
        <div className="status-pending">
          ⏳ 订单待处理
          <button>取消订单</button>
        </div>
      );
    case 'processing':
      return (
        <div className="status-processing">
          🔄 订单处理中
          <ProgressBar percent={50} />
        </div>
      );
    case 'shipped':
      return (
        <div className="status-shipped">
          📦 已发货
          <TrackingNumber number="SF123456789" />
        </div>
      );
    case 'delivered':
      return (
        <div className="status-delivered">
          ✅ 已送达
          <ConfirmReceiptButton />
        </div>
      );
    default:
      return <div className="status-unknown">❓ 未知状态</div>;
  }
}

2.5 枚举对象映射

最优雅的多分支方案,适合分支较多且逻辑简单的场景。

const STATUS_CONFIG = {
  pending: {
    icon: '⏳',
    text: '待处理',
    className: 'status-pending',
    actions: ['cancel']
  },
  processing: {
    icon: '🔄',
    text: '处理中',
    className: 'status-processing',
    actions: []
  },
  completed: {
    icon: '✅',
    text: '已完成',
    className: 'status-completed',
    actions: ['review', 'reorder']
  }
};

function OrderStatus({ status }) {
  const config = STATUS_CONFIG[status] || STATUS_CONFIG.pending;
  
  return (
    <div className={config.className}>
      <span>{config.icon}</span>
      <span>{config.text}</span>
    </div>
  );
}

// 更高级的用法:直接映射到组件
const COMPONENT_MAP = {
  loading: LoadingSpinner,
  error: ErrorMessage,
  success: SuccessMessage,
  empty: EmptyState
};

function AsyncRenderer({ status, ...props }) {
  const Component = COMPONENT_MAP[status];
  return Component ? <Component {...props} /> : null;
}

三、阻止渲染(返回 null)

某些场景下,我们希望组件被调用但不渲染任何内容。

function AdBanner({ user, isVisible }) {
  // VIP 用户不显示广告
  if (user.isVIP) {
    return null;
  }
  
  // 手动隐藏
  if (!isVisible) {
    return null;
  }
  
  return (
    <div className="ad-banner">
      限时优惠!点击购买
    </div>
  );
}

注意事项:

  • 返回 null 的组件仍然会执行生命周期(useEffect 等)
  • 父组件的 useEffect 不会因为子组件返回 null 而跳过
  • 返回 null 的组件不会被渲染到 DOM 中

四、实战案例

4.1 登录/注册切换

function AuthPage() {
  const [mode, setMode] = useState('login'); // 'login' | 'register' | 'forgot'

  const renderForm = () => {
    switch (mode) {
      case 'login':
        return <LoginForm onSwitch={setMode} />;
      case 'register':
        return <RegisterForm onSwitch={setMode} />;
      case 'forgot':
        return <ForgotPasswordForm onSwitch={setMode} />;
      default:
        return <LoginForm onSwitch={setMode} />;
    }
  };

  return (
    <div className="auth-container">
      {renderForm()}
    </div>
  );
}

4.2 数据加载状态

function DataFetcher({ url }) {
  const [state, setState] = useState({
    status: 'idle', // 'idle' | 'loading' | 'success' | 'error'
    data: null,
    error: null
  });

  useEffect(() => {
    setState({ status: 'loading', data: null, error: null });
    
    fetch(url)
      .then(res => res.json())
      .then(data => setState({ status: 'success', data, error: null }))
      .catch(error => setState({ status: 'error', data: null, error }));
  }, [url]);

  // 条件渲染的清晰写法
  if (state.status === 'loading') {
    return <LoadingSpinner />;
  }
  
  if (state.status === 'error') {
    return <ErrorMessage error={state.error} />;
  }
  
  if (state.status === 'success' && !state.data) {
    return <EmptyState />;
  }
  
  return <DataDisplay data={state.data} />;
}

4.3 权限控制

function PermissionGate({ user, requiredRole, children, fallback }) {
  const hasPermission = () => {
    const roleLevel = { admin: 3, editor: 2, viewer: 1 };
    return (roleLevel[user.role] || 0) >= (roleLevel[requiredRole] || 0);
  };

  return hasPermission() ? children : (fallback || null);
}

// 使用
<PermissionGate user={currentUser} requiredRole="editor" fallback={<NoPermission />}>
  <EditButton />
</PermissionGate>

4.4 响应式布局(根据屏幕尺寸)

function ResponsiveLayout() {
  const [isMobile, setIsMobile] = useState(window.innerWidth < 768);

  useEffect(() => {
    const handleResize = () => setIsMobile(window.innerWidth < 768);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return (
    <div>
      {isMobile ? (
        <MobileNavigation />
      ) : (
        <DesktopNavigation />
      )}
      
      {/* 只在桌面显示侧边栏 */}
      {!isMobile && <Sidebar />}
      
      <MainContent />
    </div>
  );
}

五、常见陷阱与最佳实践

5.1 陷阱:0 被渲染

// ❌ 错误:当 count = 0 时,会渲染数字 0
{count && <span>有 {count} 条消息</span>}

// ✅ 正确:明确的条件判断
{count > 0 && <span>有 {count} 条消息</span>}

// ✅ 或者使用三元运算符
{count ? <span>有 {count} 条消息</span> : null}

5.2 陷阱:字符串 ‘false’ 被渲染

// ❌ 错误:'false' 是真值,会渲染
{isVisible && <div>内容</div>}  // 假设 isVisible = 'false'

// ✅ 正确:确保是布尔值
{!!isVisible && <div>内容</div>}
{Boolean(isVisible) && <div>内容</div>}

5.3 最佳实践:提取复杂逻辑

// ❌ 不推荐:JSX 中写复杂逻辑
function Component({ user, data, loading }) {
  return (
    <div>
      {loading ? (
        <Spinner />
      ) : user ? (
        user.isVIP ? (
          <VIPDashboard data={data} />
        ) : (
          <RegularDashboard data={data} />
        )
      ) : (
        <LoginPrompt />
      )}
    </div>
  );
}

// ✅ 推荐:提取为函数
function Component({ user, data, loading }) {
  const renderContent = () => {
    if (loading) return <Spinner />;
    if (!user) return <LoginPrompt />;
    if (user.isVIP) return <VIPDashboard data={data} />;
    return <RegularDashboard data={data} />;
  };

  return <div>{renderContent()}</div>;
}

5.4 最佳实践:提前返回

// ✅ 提前返回让代码更清晰
function UserProfile({ user, loading, error }) {
  if (loading) return <LoadingSpinner />;
  if (error) return <ErrorMessage error={error} />;
  if (!user) return <LoginPrompt />;
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

六、性能优化

6.1 使用 React.memo 避免不必要的重渲染

const ExpensiveComponent = React.memo(({ data }) => {
  // 复杂计算
  return <div>{/* ... */}</div>;
});

function Parent({ showExpensive }) {
  return (
    <div>
      {/* 条件渲染不影响 ExpensiveComponent 的 memo 优化 */}
      {showExpensive && <ExpensiveComponent data={data} />}
    </div>
  );
}

6.2 懒加载条件组件

const LazyComponent = lazy(() => import('./HeavyComponent'));

function App({ showHeavy }) {
  return (
    <Suspense fallback={<div>加载中...</div>}>
      {showHeavy && <LazyComponent />}
    </Suspense>
  );
}

七、练习题

基础题

  1. 实现一个 ToggleMessage 组件:点击按钮切换显示/隐藏一段文字
  2. 实现一个 ScoreRating:根据分数(0-100)显示"优秀/良好/及格/不及格"

进阶题

  1. 实现一个 TabSwitcher:支持 3 个标签页,点击切换内容
  2. 实现一个 FormWizard:多步骤表单,根据当前步骤显示不同表单

参考答案

// 1. ToggleMessage
function ToggleMessage() {
  const [visible, setVisible] = useState(false);
  
  return (
    <div>
      <button onClick={() => setVisible(!visible)}>
        {visible ? '隐藏' : '显示'}
      </button>
      {visible && <p>这是一段可切换显示的文字</p>}
    </div>
  );
}

// 2. ScoreRating
function ScoreRating({ score }) {
  const getRating = () => {
    if (score >= 90) return { text: '优秀', color: 'green' };
    if (score >= 75) return { text: '良好', color: 'blue' };
    if (score >= 60) return { text: '及格', color: 'orange' };
    return { text: '不及格', color: 'red' };
  };
  
  const { text, color } = getRating();
  return <span style={{ color }}>评分:{text} ({score}分)</span>;
}

// 3. TabSwitcher
function TabSwitcher() {
  const [activeTab, setActiveTab] = useState(0);
  const tabs = ['Tab 1', 'Tab 2', 'Tab 3'];
  const contents = ['内容 1', '内容 2', '内容 3'];
  
  return (
    <div>
      <div className="tabs">
        {tabs.map((tab, index) => (
          <button
            key={index}
            onClick={() => setActiveTab(index)}
            style={{ fontWeight: activeTab === index ? 'bold' : 'normal' }}
          >
            {tab}
          </button>
        ))}
      </div>
      <div className="tab-content">
        {contents[activeTab]}
      </div>
    </div>
  );
}

八、小结

方式 适用场景 优点 缺点
if/else 复杂逻辑、代码块长 清晰易读 不能内联
三元运算符 简单二分支 简洁 嵌套难读
&& 运算符 条件性显示 最简洁 注意 0 陷阱
switch 多分支 结构清晰 代码冗长
枚举映射 多分支、逻辑简单 最优雅 需要额外定义

核心要点

  • 根据场景选择合适的方式
  • 避免 JSX 中嵌套过深的条件逻辑
  • 提前返回让代码更清晰
  • 注意数字 0 和字符串 ‘false’ 的陷阱
  • 复杂逻辑提取为函数或单独组件

更多推荐