高阶组件 (HOC)

什么是高阶组件?
高阶组件是一种基于 React 组合特性的高级技术。

高阶组件(Higher-Order Component,简称 HOC)是 React 中用于复用组件逻辑的高级技术。HOC 本身不是 React API 的一部分,它是一种基于 React 组合特性而形成的设计模式。

具体而言,高阶组件是一个函数,它接收一个组件作为参数,并返回一个新的增强组件。HOC 不会修改输入组件,也不会使用继承来复制其行为。相反,HOC 通过将组件包装在容器组件中来组合新组件。

// 高阶组件的基本结构
function withSomething(WrappedComponent) {
  // 返回一个新的组件
  return function(props) {
    // 可以添加新的逻辑、状态或属性
    const newProps = { ...props, somethingNew: 'value' };
    
    // 渲染被包装的组件,传入新的 props
    return <WrappedComponent {...newProps} />;
  };
}
高阶组件的常见用途
了解高阶组件的主要应用场景。

高阶组件通常用于以下场景:

  • 代码复用:提取多个组件共用的逻辑
  • 状态抽象和管理:添加状态和生命周期方法
  • props 操作:添加、修改或过滤 props
  • 渲染劫持:控制组件的渲染过程
  • 分离关注点:将业务逻辑与 UI 组件分离
// 加载状态高阶组件
function withLoading(WrappedComponent) {
  return function WithLoading({ isLoading, ...props }) {
    if (isLoading) {
      return <div>加载中...</div>;
    }
    return <WrappedComponent {...props} />;
  };
}

// 使用高阶组件
const UserListWithLoading = withLoading(UserList);

// 在父组件中使用
function App() {
  const [isLoading, setIsLoading] = useState(true);
  const [users, setUsers] = useState([]);
  
  useEffect(() => {
    fetchUsers().then(data => {
      setUsers(data);
      setIsLoading(false);
    });
  }, []);
  
  return <UserListWithLoading isLoading={isLoading} users={users} />;
}
实现一个高阶组件
学习如何创建和使用高阶组件。

下面我们将实现一个 withLogging 高阶组件,它会在组件挂载、更新和卸载时记录日志。

import React, { useEffect } from 'react';

// 创建一个记录组件生命周期的高阶组件
function withLogging(WrappedComponent) {
  // 返回一个新组件
  return function WithLogging(props) {
    const componentName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
    
    useEffect(() => {
      // 组件挂载时记录
      console.log(`[${componentName}] 已挂载`);
      
      // 组件卸载时记录
      return () => {
        console.log(`[${componentName}] 将卸载`);
      };
    }, []);
    
    // 组件渲染时记录
    console.log(`[${componentName}] 正在渲染`, props);
    
    // 渲染被包装的组件,传入所有 props
    return <WrappedComponent {...props} />;
  };
}

// 一个简单的组件
function Button({ label, onClick }) {
  return <button onClick={onClick}>{label}</button>;
}

// 使用高阶组件增强 Button
const ButtonWithLogging = withLogging(Button);

// 在父组件中使用
function App() {
  return (
    <div>
      <ButtonWithLogging 
        label="点击我" 
        onClick={() => console.log('按钮被点击')} 
      />
    </div>
  );
}

在上面的例子中,withLogging 高阶组件为被包装的组件添加了日志记录功能,而不需要修改原始组件的代码。这展示了高阶组件如何实现关注点分离和代码复用。

高阶组件的注意事项
使用高阶组件时需要注意的问题。
  • 不要在渲染方法中使用 HOC:React 的 diff 算法使用组件标识来决定是否更新现有子树或丢弃它并挂载新子树。如果渲染方法中返回的组件与前一个渲染中的组件相同,React 会递归更新子树。如果它们不同,则完全卸载前一个子树。
  • 复制静态方法:当使用 HOC 包装组件时,原始组件的静态方法不会自动复制到新组件上。需要手动复制这些方法。
  • Refs 不会传递:HOC 为组件添加了一个容器组件,这意味着新组件没有原始组件的任何静态方法。解决方案是使用 React.forwardRef API。
  • 包装显示名称以便调试:HOC 创建的组件会在 React 开发者工具中显示。为了便于调试,最好使用一个显示名称,表明它是 HOC 的结果。
// 解决静态方法复制问题
function withLogging(WrappedComponent) {
  function WithLogging(props) {
    // ...实现...
  }
  
  // 复制静态方法
  const staticMethods = Object.getOwnPropertyNames(WrappedComponent)
    .filter(name => typeof WrappedComponent[name] === 'function' && name !== 'render')
    .reduce((methods, name) => {
      methods[name] = WrappedComponent[name];
      return methods;
    }, {});
  
  Object.assign(WithLogging, staticMethods);
  
  // 设置显示名称以便调试
  WithLogging.displayName = `WithLogging(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`;
  
  return WithLogging;
}

// 解决 Refs 不会传递的问题
function withLogging(WrappedComponent) {
  function WithLogging(props, ref) {
    // ...实现...
    return <WrappedComponent {...props} ref={ref} />;
  }
  
  // 使用 forwardRef 传递 refs
  return React.forwardRef(WithLogging);
}
高阶组件 vs Hooks
比较高阶组件和 React Hooks。

随着 React Hooks 的引入,一些传统上使用高阶组件解决的问题现在可以用 Hooks 更简洁地解决。下面比较两种方法:

高阶组件

  • 包装组件,创建新组件
  • 使用组合模式
  • 可能导致组件嵌套地狱
  • 需要额外的组件实例
  • 难以传递特定的 props
  • 适合复杂的组件逻辑复用
// 使用高阶组件
const EnhancedComponent = withSomething(MyComponent);

// 使用
<EnhancedComponent />

React Hooks

  • 直接在组件内部使用
  • 使用函数调用模式
  • 避免了嵌套地狱
  • 不需要额外的组件实例
  • 可以精确控制哪些数据流入组件
  • 适合简单的状态逻辑复用
// 使用 Hooks
function MyComponent() {
  const something = useSomething();
  return <div>{something}</div>;
}

在现代 React 开发中,Hooks 通常是首选的解决方案,因为它们更简洁、更灵活。但在某些复杂场景下,高阶组件仍然有其价值,特别是在处理横切关注点(如权限控制、数据预取等)时。

最佳实践是:优先考虑使用 Hooks,当 Hooks 不足以解决问题时,再考虑使用高阶组件。