React 组件设计模式:高阶组件、Render Props 与 Hooks 对比

小飞兽 React 325 次阅读 2026-07-13

React 组件设计模式:高阶组件、Render Props 与 Hooks 对比

一、Introduction

React 有多种代码复用模式,不同模式下代码的组织方式、可复用逻辑的形态、以及性能开销都不同。在 Hooks 出现之前,高阶组件(HOC)和 Render Props 是两种主流的逻辑复用方式;Hooks 出现后,很多场景用 Hooks 更简洁。

但这不意味着 HOC 和 Render Props 已经被淘汰——它们在某些场景下仍有独特价值。本文用实战案例讲解三种模式的用法、优缺点,以及在什么场景下该选哪种。

---

二、Render Props 模式

#### 2.1 核心思想

Render Props 的核心是"把渲染逻辑作为 prop 传入组件",让父组件控制子组件的渲染内容,同时复用逻辑层的代码:

import React, { useState, useEffect } from 'react';

// ==================== Render Props: 鼠标位置追踪 ====================

class MouseTracker extends React.Component {
  state = { x: 0, y: 0 };

  handleMouseMove = (e) => {
    this.setState({ x: e.clientX, y: e.clientY });
  };

  componentDidMount() {
    window.addEventListener('mousemove', this.handleMouseMove);
  }

  componentWillUnmount() {
    window.removeEventListener('mousemove', this.handleMouseMove);
  }

  render() {
    // 把状态作为 props 传给 render 函数
    return this.props.render(this.state);
  }
}

// 使用方:完全自定义渲染方式
function MouseTrackerDemo() {
  return (
    <div>
      <h3>Render Props 示例</h3>
      <MouseTracker
        render={({ x, y }) => (
          <div>
            <p>鼠标位置:({x}, {y})</p>
            <div
              style={{
                width: 20,
                height: 20,
                borderRadius: '50%',
                background: 'red',
                position: 'fixed',
                left: x - 10,
                top: y - 10,
                pointerEvents: 'none'
              }}
            />
          </div>
        )}
      />
    </div>
  );
}

// ==================== Render Props: 数据请求复用 ====================

class DataFetcher extends React.Component {
  state = { data: null, loading: true, error: null };

  componentDidMount() {
    this.fetchData();
  }

  componentDidUpdate(prevProps) {
    if (prevProps.url !== this.props.url) {
      this.fetchData();
    }
  }

  fetchData = async () => {
    this.setState({ loading: true, error: null });
    try {
      const response = await fetch(this.props.url);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      const data = await response.json();
      this.setState({ data, loading: false });
    } catch (err) {
      this.setState({ error: err.message, loading: false });
    }
  };

  render() {
    return this.props.render(this.state);
  }
}

function PostsList() {
  const [postId, setPostId] = useState(1);

  return (
    <div>
      <h3>数据请求复用示例</h3>
      <button onClick={() => setPostId(id => Math.max(1, id - 1))}>上一篇</button>
      <span style={{ margin: '0 12px' }}>第 {postId} 篇</span>
      <button onClick={() => setPostId(id => id + 1)}>下一篇</button>

      <DataFetcher
        url={`https://jsonplaceholder.typicode.com/posts/${postId}`}
        render={({ data, loading, error }) => {
          if (loading) return <p>加载中...</p>;
          if (error) return <p>错误:{error}</p>;
          return (
            <div style={{ marginTop: 12 }}>
              <h4>{data.title}</h4>
              <p>{data.body}</p>
            </div>
          );
        }}
      />
    </div>
  );
}

---

三、高阶组件(HOC)模式

#### 3.1 核心思想

HOC 是一个函数,接收一个组件作为参数,返回一个增强了的新组件。它通过包装原组件,在原组件的外层添加额外的功能:

import React from 'react';

// ==================== HOC: 权限控制 ====================

// withAuth:检查用户是否已登录,未登录则重定向
function withAuth(WrappedComponent) {
  return function AuthenticatedComponent(props) {
    const [isAuthenticated, setIsAuthenticated] = React.useState(false);
    const [isLoading, setIsLoading] = React.useState(true);

    React.useEffect(() => {
      // 模拟检查登录状态
      const checkAuth = () => {
        const token = localStorage.getItem('auth_token');
        setIsAuthenticated(!!token);
        setIsLoading(false);
      };
      checkAuth();
    }, []);

    if (isLoading) return <div>检查登录状态中...</div>;
    if (!isAuthenticated) {
      return <div>🔒 请先登录后再访问此页面</div>;
    }

    // 把原 props 透传给被包装的组件
    return <WrappedComponent {...props} />;
  };
}

// ==================== HOC: 添加 Loading 状态 ====================

function withLoading(WrappedComponent) {
  return function WithLoadingComponent({ isLoading, loadingIndicator, ...props }) {
    if (isLoading) {
      return loadingIndicator || <div style={{ padding: 20, textAlign: 'center' }}>⏳ 加载中...</div>;
    }
    return <WrappedComponent {...props} />;
  };
}

// ==================== HOC: 控制台日志(调试用)====================

function withLogger(WrappedComponent) {
  return function LoggerComponent(props) {
    React.useEffect(() => {
      console.log(`[${WrappedComponent.name}] 挂载`);
      return () => console.log(`[${WrappedComponent.name}] 卸载`);
    }, []);

    console.log(`[${WrappedComponent.name}] 渲染,props:`, props);
    return <WrappedComponent {...props} />;
  };
}

// ==================== 使用示例 ====================

class OriginalDashboard extends React.Component {
  render() {
    return (
      <div>
        <h2>仪表盘(class 组件)</h2>
        <p>当前用户:{this.props.username}</p>
        <p>用户等级:{this.props.userLevel}</p>
      </div>
    );
  }
}

// 用 HOC 包装
const DashboardWithAuth = withAuth(OriginalDashboard);
const DashboardWithLoading = withLoading(DashboardWithAuth);
const DashboardLogged = withLogger(DashboardWithLoading);

// 模拟使用(username 和 userLevel 是 HOC 透传下来的 props)
function HOCDemo() {
  const [isLoading, setIsLoading] = React.useState(true);

  React.useEffect(() => {
    setTimeout(() => setIsLoading(false), 2000);
  }, []);

  return (
    <div>
      <h3>HOC 示例(2秒后显示仪表盘)</h3>
      <DashboardLogged
        isLoading={isLoading}
        username="管理员"
        userLevel={99}
      />
    </div>
  );
}

---

四、三种模式对比与选择指南

| 维度 | Render Props | HOC | Hooks |
|------|-------------|-----|-------|
| 学习成本 | 中等 | 较高 | 低 |
| 代码量 | 少(嵌套少时) | 多(需要包装) | 最少 |
| Props 冲突 | 无(显式传递) | 有(props 命名冲突) | 无 |
| 静态组合 | 困难 | 容易 | 容易 |
| 适用场景 | 状态复用+自定义渲染 | 横切关注点(权限、日志) | 逻辑复用(推荐优先用) |

选择建议:

  • 优先用 <strong>Hooks</strong>:大多数逻辑复用场景,Hooks 更直观

  • 用 <strong>Render Props</strong>:当需要让使用者自定义渲染逻辑时

  • 用 <strong>HOC</strong>:当需要在组件挂载/卸载时注入横切逻辑时

---

五、运行效果说明

MouseTrackerDemo:在页面上移动鼠标,红色圆点跟随鼠标移动,坐标实时显示。Render Props 把鼠标坐标暴露给使用方,使用方完全控制渲染方式——可以是坐标文字、可以是跟随圆点、可以是任何自定义 UI。

PostsList:点击"上一篇/下一篇"切换文章 ID,DataFetcher 自动发起新请求,显示加载状态,请求完成后显示文章标题和正文。

HOCDemo:DashboardWrapped 组件在 2 秒内显示加载状态(withLoading),2 秒后显示仪表盘内容(withAuth 控制是否允许访问),同时控制台打印挂载/卸载/渲染日志(withLogger)。

---

六、常见问题(FAQ)

#### Q1:Hooks 出来之后,HOC 和 Render Props 还有价值吗?

有价值,但应该优先用 Hooks。HOC 的独特价值在于:可以在 class 组件中使用(当你要给已有的 class 组件添加功能时),以及通过静态组合实现"装饰器"式的功能叠加。Render Props 在需要"状态共享 + 渲染自定义"时仍然优雅。

// Hooks 无法做到的事:给 class 组件添加功能
const EnhancedClassComponent = withAuth(ClassComponent); // HOC 可以做到

// 但给函数组件加功能,Hooks 是最优解
function FunctionalComponent() {
  const { user } = useAuth(); // 清晰直观
}

#### Q2:HOC 会出现 Props 命名冲突吗?

是的,这是 HOC 最大的缺点。如果 HOC 没有正确地把 props 透传给被包装组件,或者多个 HOC 都试图设置同一个 prop 名称,就会产生冲突。解决方案:使用 hoist-non-react-statics 库自动复制静态方法,并确保所有非 React 的 props 都透传下去。

#### Q3:Render Props 会形成"嵌套地狱"吗?

会的。和 HOC 的"包装嵌套"问题类似,如果多个组件都使用 Render Props 模式,会形成多层 render 函数嵌套。Hooks 解决了这个问题——多个自定义 Hook 可以自由组合,不形成嵌套。

---

六、延伸阅读

  • [React Hooks 完全指南:useState 与 useEffect 核心用法]()
  • [React 自定义 Hooks 实战:逻辑复用与模块化]()
  • [React Context API 状态管理深度理解]()
  • [React 状态管理演进:从 useState 到 Redux]()

---