React 高阶组件(HOC)

高阶组件(Higher-Order Component)是React中用于复用组件逻辑的高级技术。它是一个函数,接收一个组件并返回一个新的增强组件。

什么是高阶组件?

高阶组件是React中一种基于组件组合的设计模式,本质上是一个函数,它接收一个组件作为参数,并返回一个新的组件。

原始组件
HOC函数
增强组件
输入

原始React组件

处理

HOC添加额外功能

输出

增强的React组件

HOC的基本结构

import React from 'react';

// 高阶组件函数
const withEnhancement = (WrappedComponent) => {
  // 返回一个新的组件类
  class EnhancedComponent extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        // 添加额外的状态
        isEnhanced: true,
        data: null
      };
    }

    componentDidMount() {
      // 添加生命周期方法
      console.log('组件已被增强');
    }

    // 添加额外的方法
    fetchData = () => {
      // 数据获取逻辑
    };

    render() {
      // 将额外的props传递给被包装的组件
      const enhancedProps = {
        ...this.props,
        isEnhanced: this.state.isEnhanced,
        fetchData: this.fetchData,
        hocData: '来自HOC的数据'
      };

      return <WrappedComponent {...enhancedProps} />;
    }
  }

  // 设置displayName以便调试
  EnhancedComponent.displayName = `WithEnhancement(${getDisplayName(WrappedComponent)})`;

  return EnhancedComponent;
};

// 辅助函数:获取组件显示名称
function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

// 使用HOC增强组件
const OriginalComponent = (props) => {
  return (
    <div>
      <h3>原始组件</h3>
      <p>HOC传递的数据: {props.hocData}</p>
      {props.isEnhanced && <p>✅ 此组件已被增强</p>}
    </div>
  );
};

const EnhancedComponent = withEnhancement(OriginalComponent);

// 使用增强后的组件
function App() {
  return <EnhancedComponent someProp="来自App的数据" />;
}

常见HOC模式

认证HOC

保护路由,验证用户权限

const withAuth = (WrappedComponent) => {
  return class extends React.Component {
    componentDidMount() {
      if (!isAuthenticated()) {
        redirectToLogin();
      }
    }
    render() {
      return isAuthenticated() ?
        <WrappedComponent {...this.props} /> :
        <LoginPrompt />;
    }
  };
};
数据获取HOC

统一处理数据加载逻辑

const withData = (url) => (WrappedComponent) => {
  return class extends React.Component {
    state = { data: null, loading: true };

    async componentDidMount() {
      const data = await fetch(url);
      this.setState({ data, loading: false });
    }

    render() {
      return <WrappedComponent
        data={this.state.data}
        loading={this.state.loading}
        {...this.props}
      />;
    }
  };
};
性能监控HOC

跟踪组件渲染性能

const withPerformance = (WrappedComponent) => {
  return class extends React.Component {
    componentDidMount() {
      this.startTime = performance.now();
    }

    componentDidUpdate() {
      const renderTime = performance.now() - this.startTime;
      console.log(`${WrappedComponent.name} 渲染耗时: ${renderTime}ms`);
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
};
国际化HOC

提供多语言支持

const withI18n = (WrappedComponent) => {
  return class extends React.Component {
    t = (key) => {
      const translations = {
        'welcome': '欢迎',
        'submit': '提交'
      };
      return translations[key] || key;
    };

    render() {
      return <WrappedComponent
        t={this.t}
        {...this.props}
      />;
    }
  };
};

HOC vs Hooks

高阶组件(HOC)

  • 包装组件,返回新组件
  • 基于组件组合
  • 可能引起嵌套地狱
  • 易于调试(React DevTools)
  • Class组件时代的主流
  • 可以访问生命周期方法

Hooks

  • 函数内调用,直接复用逻辑
  • 基于函数调用
  • 避免嵌套问题
  • 需要额外学习成本
  • 现代React推荐方式
  • 无法访问生命周期
选择建议:
  • 使用HOC的场景:需要增强整个组件,包含生命周期,兼容老代码
  • 使用Hooks的场景:复用状态逻辑,避免嵌套,函数组件环境
  • 可以结合使用:HOC包装使用Hooks的组件

实用HOC示例

1. 错误边界HOC

import React from 'react';

const withErrorBoundary = (WrappedComponent) => {
  class ErrorBoundary extends React.Component {
    constructor(props) {
      super(props);
      this.state = { hasError: false, error: null };
    }

    static getDerivedStateFromError(error) {
      return { hasError: true, error };
    }

    componentDidCatch(error, errorInfo) {
      // 可以在这里记录错误到日志服务
      console.error('组件错误:', error, errorInfo);
    }

    render() {
      if (this.state.hasError) {
        return (
          <div style={{
            padding: '20px',
            border: '2px solid #dc3545',
            borderRadius: '8px',
            background: '#f8d7da',
            color: '#721c24'
          }}>
            <h3>组件发生错误</h3>
            <p>错误信息: {this.state.error.toString()}</p>
            <button
              onClick={() => this.setState({ hasError: false })}
              style={{
                padding: '8px 16px',
                background: '#dc3545',
                color: 'white',
                border: 'none',
                borderRadius: '4px',
                cursor: 'pointer'
              }}
            >
              重试
            </button>
          </div>
        );
      }

      return <WrappedComponent {...this.props} />;
    }
  }

  ErrorBoundary.displayName = `WithErrorBoundary(${WrappedComponent.name})`;
  return ErrorBoundary;
};

// 使用示例
const BuggyComponent = () => {
  // 故意抛出错误
  throw new Error('测试错误');
  return <div>正常显示</div>;
};

const SafeComponent = withErrorBoundary(BuggyComponent);

2. 鼠标跟踪HOC

const withMousePosition = (WrappedComponent) => {
  return class extends React.Component {
    state = {
      x: 0,
      y: 0
    };

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

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

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

    render() {
      return (
        <WrappedComponent
          mouseX={this.state.x}
          mouseY={this.state.y}
          {...this.props}
        />
      );
    }
  };
};

// 使用示例
const DisplayMousePosition = (props) => {
  return (
    <div style={{ padding: '20px', border: '1px solid #ccc' }}>
      <h3>鼠标位置追踪</h3>
      <p>X坐标: {props.mouseX}</p>
      <p>Y坐标: {props.mouseY}</p>
    </div>
  );
};

const MouseTrackingComponent = withMousePosition(DisplayMousePosition);

3. 加载状态HOC

const withLoading = (WrappedComponent) => {
  return class extends React.Component {
    state = {
      loading: true,
      data: null
    };

    async componentDidMount() {
      try {
        // 模拟API调用
        const response = await fetch(this.props.url);
        const data = await response.json();
        this.setState({ data, loading: false });
      } catch (error) {
        this.setState({ loading: false });
        console.error('数据加载失败:', error);
      }
    }

    render() {
      const { loading, data } = this.state;

      if (loading) {
        return (
          <div style={{
            display: 'flex',
            justifyContent: 'center',
            alignItems: 'center',
            height: '200px'
          }}>
            <div className="spinner-border" role="status">
              <span className="visually-hidden">加载中...</span>
            </div>
          </div>
        );
      }

      return <WrappedComponent data={data} {...this.props} />;
    }
  };
};

// 使用示例
const UserList = ({ data }) => {
  return (
    <ul>
      {data.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
};

const UserListWithLoading = withLoading(UserList);

组合多个HOC

import React from 'react';

// 多个HOC函数
const withLogger = (WrappedComponent) => {
  return class extends React.Component {
    componentDidMount() {
      console.log(`组件 ${WrappedComponent.name} 已挂载`);
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
};

const withTimer = (WrappedComponent) => {
  return class extends React.Component {
    state = { time: new Date() };

    componentDidMount() {
      this.timerId = setInterval(() => {
        this.setState({ time: new Date() });
      }, 1000);
    }

    componentWillUnmount() {
      clearInterval(this.timerId);
    }

    render() {
      return <WrappedComponent
        currentTime={this.state.time}
        {...this.props}
      />;
    }
  };
};

const withStyle = (styles) => (WrappedComponent) => {
  return class extends React.Component {
    render() {
      return (
        <div style={styles}>
          <WrappedComponent {...this.props} />
        </div>
      );
    }
  };
};

// 基础组件
const BasicComponent = (props) => {
  return (
    <div>
      <h3>基础组件</h3>
      <p>当前时间: {props.currentTime.toLocaleTimeString()}</p>
      <p>其他属性: {props.message}</p>
    </div>
  );
};

// 组合多个HOC的方式1:嵌套调用
const EnhancedComponent1 = withLogger(withTimer(withStyle({
  padding: '20px',
  border: '2px solid #007bff',
  borderRadius: '8px'
})(BasicComponent)));

// 组合多个HOC的方式2:使用compose函数(类似redux)
const compose = (...funcs) => {
  return funcs.reduce((a, b) => (...args) => a(b(...args)));
};

const enhance = compose(
  withLogger,
  withTimer,
  withStyle({
    padding: '20px',
    border: '2px solid #28a745',
    borderRadius: '8px'
  })
);

const EnhancedComponent2 = enhance(BasicComponent);

// 使用
function App() {
  return (
    <div>
      <EnhancedComponent1 message="嵌套调用方式" />
      <EnhancedComponent2 message="compose函数方式" />
    </div>
  );
}

HOC演示区

点击计数器HOC演示

悬停高亮HOC演示

将鼠标移到这里查看高亮效果

主题切换HOC演示

当前主题:浅色

HOC最佳实践

1. 保持props透传

确保所有原始props都传递给被包装组件

render() {
  // 正确:透传所有props
  return <WrappedComponent {...this.props} />;

  // 错误:可能会丢失props
  // return <WrappedComponent />;
}
2. 使用displayName便于调试
EnhancedComponent.displayName =
  `WithEnhancement(${getDisplayName(WrappedComponent)})`;
3. 不要修改原始组件

使用组合而非继承,避免直接修改组件原型

4. 将静态方法复制到新组件
function copyStaticMethods(source, target) {
  Object.keys(source).forEach(key => {
    if (typeof source[key] === 'function') {
      target[key] = source[key];
    }
  });
}
5. 使用ref转发
const withRefForwarding = (WrappedComponent) => {
  class EnhancedComponent extends React.Component {
    render() {
      const { forwardedRef, ...rest } = this.props;
      return <WrappedComponent ref={forwardedRef} {...rest} />;
    }
  }

  return React.forwardRef((props, ref) => {
    return <EnhancedComponent {...props} forwardedRef={ref} />;
  });
};

优点

  • 逻辑复用:轻松复用横切关注点
  • 组合灵活:可以组合多个HOC
  • 调试友好:在React DevTools中清晰可见
  • 向后兼容:兼容Class组件和函数组件
  • 社区成熟:大量现成的HOC库可用

缺点

  • 嵌套地狱:多个HOC可能导致深度嵌套
  • props冲突:不同HOC可能使用相同prop名
  • 静态方法丢失:需要手动复制静态方法
  • ref问题:需要额外处理ref转发
  • 复杂性:增加代码理解和调试难度

HOC与Render Props对比

// HOC实现方式
const withDataHOC = (url) => (WrappedComponent) => {
  return class extends React.Component {
    state = { data: null };
    // ... 数据获取逻辑
    render() {
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
};

// Render Props实现方式
class DataProvider extends React.Component {
  state = { data: null };
  // ... 相同的数据获取逻辑

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

// 使用Render Props
<DataProvider url="/api/data">
  {(data) => data ? <Component data={data} /> : <Loading />}
</DataProvider>

现代React中的HOC使用建议

重要提示:
  • 优先考虑Hooks:在新的项目中优先使用自定义Hooks
  • HOC仍有用处:对于需要增强整个组件的场景,HOC依然有效
  • 结合使用:可以在HOC内部使用Hooks,或在Hooks组件上使用HOC
  • 逐步迁移:将现有的HOC逐步重写为Hooks
  • 了解两者:理解HOC有助于维护老项目和阅读现有代码

常见HOC库

库名 用途 特点
recompose React工具库 提供大量实用的HOC,已被Hooks替代
react-redux 状态管理 connect()函数是最著名的HOC
react-router 路由管理 withRouter()提供路由相关props
react-i18next 国际化 withTranslation()提供翻译功能
material-ui UI组件库 使用HOC增强样式和功能
HOC交互控制台
输入 'help' 查看可用命令