React——高阶组件

260 阅读3分钟

一. 概念

至少满足以下一个条件:

  1. 接受一个或多个函数作为输入
  2. 输出一个函数
const EnhancedComponent = highOrderComponent(WrappedComponent);

二、 基本写法

  1. 不需要传递参数时

    function withRouter(){
        return class wrapComponent extends React.Component{
            /* 编写逻辑 */
        }
    }
    
  2. 需要传递参数时

    function connect (mapStateToProps){
        /* 接受第一个参数 */
        return function connectAdvance(wrapCompoent){
            /* 接受组件 */
            return class WrapComponent extends React.Component{  }
        }
    }
    

三、需遵守的约定

  1. 不要改变原始组件

    • 如修改组件原型(以下修改不被允许)

      function logProps(InputComponent) {
        InputComponent.prototype.componentDidUpdate = function(prevProps) {
          console.log('Current props: ', this.props);
          console.log('Previous props: ', prevProps);
        };
        return InputComponent; // 返回被污染的原始组件
      }
      
      const Composed = trackAnalytics(logProps(InputComponent));
      
      • 可能导致组件无法像HOC增强之前那样使用
      • 也可能会导致另一个修改componentDidUpdate的HOC失效
    • 应该使用组合的方式来实现

      function logProps(WrappedComponent) {
        // 返回全新的容器组件
        return class extends React.Component {
          componentDidUpdate(prevProps) {
            console.log('Current props: ', this.props);
            console.log('Previous props: ', prevProps);
          }
          render() {
            // 通过组合包裹原始组件
            return <WrappedComponent {...this.props} />;
          }
        }
      }
      
      const Enhanced = logProps(trackAnalytics(InputComponent));
      

  2. 将不相关的props传递给被包裹的组件

    render() {
      // 分离出 HOC 自用的 props 和其他 props
      const { extraProp, ...passThroughProps } = this.props;
    
      // HOC 可能注入的新 props(如状态/方法)
      const injectedProp = someStateOrInstanceMethod;
    
      // 透传无关 props + 注入新 props
      return (
        <WrappedComponent
          injectedProp={injectedProp}   // HOC 新增的 prop
          {...passThroughProps}         // 透传其他所有 props
        />
      );
    }
    
    • 为什么要透传?

      • 避免props丢失

        若 HOC 不传递 classNameonClick 等通用 props,被包裹组件可能无法正常使用样式或事件。

  3. 最大化可组合性

    让 HOC 更容易组合在一起使用,避免深层嵌套,提高代码可读性和维护性。

    // 不推荐如下写法...
    const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))
    
    // ... 建议编写组合工具函数
    // compose(f, g, h) 等同于 (...args) => f(g(h(...args)))
    const enhance = compose(
      // 这些都是单参数的 HOC
      withRouter,
      connect(commentSelector)
    )
    const EnhancedComponent = enhance(WrappedComponent)
    
  4. 包装显示名称

    当在 React Developer Tools 中检查由 HOC 创建的组件时,它们默认显示为类似 <Anonymous><Component> 的名称,难以区分和追踪。

    在 HOC 内部,为它返回的包装组件设置一个明确的 displayName 属性可以解决这个问题,如以下代码,组件树会清晰显示为 WithSubscription(CommentList)

    function withSubscription(WrappedComponent) {
      class WithSubscription extends React.Component { /* ... */ }
    
      // 关键:设置易识别的 displayName
      WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
    
      return WithSubscription;
    }
    
    // 辅助函数:安全地获取组件名称
    function getDisplayName(WrappedComponent) {
      return WrappedComponent.displayName || WrappedComponent.name || 'Component';
    }
    

四、注意事项

  1. 不要再render方法中使用HOC

    React 的 diff 算法(称为协调)使用组件标识来确定它是应该更新现有子树还是将其丢弃并挂载新子树。 如果从 render 返回的组件与前一个渲染中的组件相同(===),则 React 通过将子树与新子树进行区分来递归更新子树。 如果它们不相等,则完全卸载前一个子树。

    render() {
      // 每次调用 render 函数都会创建一个新的 EnhancedComponent
      // EnhancedComponent1 !== EnhancedComponent2
      const EnhancedComponent = enhance(MyComponent);
      // 这将导致子树每次渲染都会进行卸载,和重新挂载的操作!
      return <EnhancedComponent />;
    }
    
    • 导致性能问题以及重新挂载组件会导致该组件及其所有子组件的状态丢失
    • 在极少数情况下需要动态调用 HOC,可以在组件的生命周期方法或其构造函数中进行调用
  2. 要复制静态方法

    • 当组件被HOC包装时,原始组件的静态方法不会自动传递到新组件

      // 原始组件定义静态方法
      WrappedComponent.staticMethod = function() {/*...*/}
      
      // 使用 HOC 包装
      const EnhancedComponent = enhance(WrappedComponent);
      
      // 增强后的组件丢失静态方法
      typeof EnhancedComponent.staticMethod === 'undefined' // true
      
    • 解决方法

      • 推荐使用 hoist-non-react-statics

        import hoistNonReactStatic from 'hoist-non-react-statics';
        
        function enhance(WrappedComponent) {
          class Enhance extends React.Component {/*...*/}
          // 自动复制所有非 React 静态方法
          hoistNonReactStatic(Enhance, WrappedComponent);
          return Enhance;
        }
        
      • 分离导出

        // 原始文件 MyComponent.js
        export function someFunction() { /* 静态方法逻辑 */ } // 单独导出方法
        export default MyComponent; // 导出组件
        
        // 使用方
        import MyComponent, { someFunction } from './MyComponent.js';
        
  3. Refs不会被传递

    • ref 实际上并不是一个 prop,它是由 React 专门处理的。如果将 ref 添加到 HOC 的返回组件中,则 ref 引用指向容器组件,而不是被包装组件

      <EnhancedComponent ref={myRef} /> 
      // myRef 指向的是 HOC 容器组件,而非原始组件
      
    • 解决方法

      • 使用 React.forwardRef

        • 原理:forwardRef 创建特殊组件,将接收到的 ref 通过第二个参数转发到内部组件。通常结合 HOC 使用
        const EnhancedComponent = React.forwardRef((props, ref) => {
          // 将 ref 作为特殊 prop(如 innerRef)传递给原始组件
          return <WrappedComponent {...props} innerRef={ref} />;
        });
        
        // 使用
        <EnhancedComponent ref={myRef} /> // myRef 现在指向原始组件
        

五、使用场景

  1. 注入props

    • 场景:当多个组件需要访问相同的数据源(如用户信息、主题配置等)时,使用HOC统一注入这些属性,避免在每个组件中重复获取逻辑。
    // 高阶组件:注入用户信息
    function withUser(WrappedComponent) {
      return function(props) {
        const user = { name: "John", role: "admin" }; 
        return <WrappedComponent {...props} user={user} />;
      };
    }
    
    // 普通组件
    const Profile = ({ user }) => (
      <div>
        <h2>User Profile</h2>
        <p>Name: {user.name}</p>
        <p>Role: {user.role}</p>
      </div>
    );
    
    // 使用高阶组件
    const EnhancedProfile = withUser(Profile);
    
  2. 状态管理

    • 场景:封装可复用的状态逻辑(如计数器、表单状态),使多个组件共享相同的行为而不重复代码。
    // 高阶组件:管理计数器状态
    function withCounter(WrappedComponent) {
      return class extends React.Component {
        state = { count: 0 };
        
        increment = () => this.setState(prev => ({ count: prev.count + 1 }));
        decrement = () => this.setState(prev => ({ count: prev.count - 1 }));
        
        render() {
          return (
            <WrappedComponent
              {...this.props}
              count={this.state.count}
              increment={this.increment}
              decrement={this.decrement}
            />
          );
        }
      };
    }
    
    // 普通组件
    const Counter = ({ count, increment, decrement }) => (
      <div>
        <button onClick={decrement}>-</button>
        <span>{count}</span>
        <button onClick={increment}>+</button>
      </div>
    );
    
    // 使用高阶组件
    const EnhancedCounter = withCounter(Counter);
    
  3. 条件渲染

    • 场景:根据用户权限控制组件渲染,实现安全边界保护敏感内容。
    // 高阶组件:权限守卫
    function withAuth(requiredRole) {
      // 返回接受组件的函数
      return function(WrappedComponent) {
        // 返回条件渲染组件
        return function(props) {
          // 实际从认证系统获取
          const currentRole = localStorage.getItem('userRole') || 'guest';
          
          // 权限检查
          if (currentRole !== requiredRole) {
            return (
              <div className="unauthorized">
                <h3>Access Denied</h3>
                <p>You need {requiredRole} privileges to view this content</p>
              </div>
            );
          }
          
          // 通过检查则渲染组件
          return <WrappedComponent {...props} />;
        };
      };
    }
    
    // 管理员面板组件
    const AdminDashboard = () => (
      <div className="admin-panel">
        <h2>Admin Controls</h2>
        <p>Sensitive operations here...</p>
      </div>
    );
    
    // 保护组件:仅允许admin访问
    const ProtectedAdminDashboard = withAuth("admin")(AdminDashboard);
    
  4. 数据获取

  • 场景:封装数据获取的通用模式(加载/错误/数据状态),简化组件中的数据请求逻辑。

    // 高阶组件:数据获取器
    function withDataFetching(url) {
      // 返回接受组件的函数
      return function(WrappedComponent) {
        // 返回数据管理组件
        return class extends React.Component {
          state = { 
            data: null, 
            loading: true, 
            error: null 
          };
          
          async componentDidMount() {
            try {
              const response = await fetch(url);
              if (!response.ok) throw new Error('Network error');
              const json = await response.json();
              this.setState({ data: json, loading: false });
            } catch (error) {
              this.setState({ error, loading: false });
            }
          }
          
          render() {
            const { data, loading, error } = this.state;
            
            // 状态处理
            if (loading) return <div className="loader">Loading data...</div>;
            if (error) return <div className="error">Error: {error.message}</div>;
            if (!data) return <div>No data available</div>;
            
            // 传递数据给被包装组件
            return <WrappedComponent {...this.props} data={data} />;
          }
        };
      };
    }
    
    // 原始组件 - 纯数据展示
    const UserList = ({ data }) => (
      <ul className="user-list">
        {data.map(user => (
          <li key={user.id}>
            {user.name} - {user.email}
          </li>
        ))}
      </ul>
    );
    
    // 增强后的数据组件
    const UserListWithData = withDataFetching("https://api.example.com/users")(UserList);
    
  1. 性能优化

    • 场景:通过自动实现shouldComponentUpdate优化组件渲染性能,特别适合数据量大的列表/表格。

      // 高阶组件:纯渲染优化
      function withPureRender(WrappedComponent) {
        // 返回继承自PureComponent的包装组件
        return class extends React.PureComponent {
          render() {
            return <WrappedComponent {...this.props} />;
          }
        };
      }
      
      // 复杂数据网格组件
      class DataGrid extends React.Component {
        render() {
          // 假设这里有昂贵的渲染操作
          const { data } = this.props;
          return (
            <table className="data-grid">
              <thead>...</thead>
              <tbody>
                {data.map(row => (
                  <tr key={row.id}>
                    {Object.values(row).map(cell => (
                      <td key={cell.id}>{cell.value}</td>
                    ))}
                  </tr>
                ))}
              </tbody>
            </table>
          );
        }
      }
      
      // 优化后的网格组件
      const OptimizedDataGrid = withPureRender(DataGrid);
      
  2. 上下文集成

    • 场景:将上下文(如主题、语言包)的值作为props注入,避免组件直接依赖Context API。

      // 高阶组件:主题提供器
      function withTheme(WrappedComponent) {
        // 返回函数组件(使用Context)
        return function(props) {
          // 实际项目中从ThemeContext获取
          const theme = {
            primaryColor: "#3498db",
            secondaryColor: "#2ecc71",
            textColor: "#333"
          };
          
          return <WrappedComponent {...props} theme={theme} />;
        };
      }
      
      // 主题化按钮组件
      const ThemedButton = ({ theme, children }) => (
        <button 
          style={{
            backgroundColor: theme.primaryColor,
            color: theme.textColor,
            border: `1px solid ${theme.secondaryColor}`
          }}
        >
          {children}
        </button>
      );
      
      // 增强后的主题组件
      const EnhancedButton = withTheme(ThemedButton);
      
  3. 行为跟踪

    • 场景:在组件生命周期中添加通用行为跟踪(如日志记录、性能监控、分析事件)。

      // 高阶组件:生命周期记录器
      function withLogger(WrappedComponent) {
        // 返回类组件
        return class extends React.Component {
          componentDidMount() {
            console.log(`[Lifecycle] ${WrappedComponent.name} mounted`);
            // 实际项目可能发送到分析服务
            analytics.track('component_mounted', { 
              name: WrappedComponent.name 
            });
          }
          
          componentDidUpdate(prevProps) {
            console.log(`[Lifecycle] ${WrappedComponent.name} updated`, {
              prevProps,
              currentProps: this.props
            });
          }
          
          componentWillUnmount() {
            console.log(`[Lifecycle] ${WrappedComponent.name} unmounted`);
          }
          
          render() {
            return <WrappedComponent {...this.props} />;
          }
        };
      }
      
      // 普通页面组件
      class ProductPage extends React.Component {
        render() {
          return <div>Product Details Page</div>;
        }
      }
      
      // 增强后的可追踪组件
      const TrackedProductPage = withLogger(ProductPage);
      
  4. 表单处理

    • 场景:抽象表单状态管理(值收集、验证、提交),简化表单组件的复杂度。

      // 高阶组件:表单状态管理
      function withFormState(WrappedComponent) {
        // 返回表单状态组件
        return class extends React.Component {
          state = { 
            formData: {},
            errors: {} 
          };
          
          // 统一变更处理器
          handleChange = (field, value) => {
            this.setState(prev => ({
              formData: { ...prev.formData, [field]: value },
              errors: { ...prev.errors, [field]: null }
            }));
          };
          
          // 表单提交处理
          handleSubmit = (onSuccess) => {
            const errors = this.validate(this.state.formData);
            if (Object.keys(errors).length > 0) {
              this.setState({ errors });
            } else {
              onSuccess(this.state.formData);
            }
          };
          
          // 简单验证示例
          validate = (data) => {
            const errors = {};
            if (!data.username) errors.username = "Username required";
            if (!data.email?.includes("@")) errors.email = "Invalid email";
            return errors;
          };
          
          render() {
            return (
              <WrappedComponent
                {...this.props}
                formData={this.state.formData}
                errors={this.state.errors}
                onFormChange={this.handleChange}
                onFormSubmit={this.handleSubmit}
              />
            );
          }
        };
      }
      
      // 表单UI组件
      const LoginForm = ({ formData, errors, onFormChange, onFormSubmit }) => (
        <form onSubmit={e => { e.preventDefault(); onFormSubmit(console.log); }}>
          <input
            name="username"
            value={formData.username || ""}
            onChange={e => onFormChange("username", e.target.value)}
            placeholder="Username"
          />
          {errors.username && <span className="error">{errors.username}</span>}
          
          <input
            type="email"
            name="email"
            value={formData.email || ""}
            onChange={e => onFormChange("email", e.target.value)}
            placeholder="Email"
          />
          {errors.email && <span className="error">{errors.email}</span>}
          
          <button type="submit">Login</button>
        </form>
      );
      
      // 增强后的表单组件
      const EnhancedLoginForm = withFormState(LoginForm);