React

深入 React 高阶组件

By John Han
Picture of the author
Published on
image alt attribute

深入 React 高阶组件

解决关注点横切,抽取出组件的公共部分,是大型应用的必备工作。

1. 关注点横切

目的:?组件抽象?

关注点是是什么 ❓

关注点指某个功能(如日志)。

横切关注点是指面向对象中某个关注点在多个模块中使用(如日志模块)。

2. 如何解决关注点横切

3.mixin

3.1 什么是 mixin

3.2 如何使用 mixin

3.3 mixin 使用技巧

3.4 mixin 的应用

3.5 mixin 注意事项

4. 高阶组件

4.1 高阶组件是什么?

高阶组件(HOC)是一种基于 React 的组合特性而形成的设计模式

具体而言,高阶组件是参数为组件,返回值为新组件的函数

可以看出,HOC 的主要作用是复用组件

const EnhancedComponent = higherOrderComponent(WrappedComponent);

1.2.1 高阶组件解决横切关注点问题

如果我们需要在不同的模块中做以下事情:

  • 在挂载时,向 DataSource 添加一个更改侦听器(用于获取数据)
  • 在侦听器内部,当数据源发生变化时,调用 setState(设置数据)
  • 在卸载时,删除侦听器(删除监听器)

CommentList 和 BlogPost 两个组件都需要获取评论数据:

const CommentListWithSubscription = withSubscription(
  CommentList,
  (DataSource) => DataSource.getComments()
);

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)
);

第一个参数是被包装组件

第二个参数通过 DataSource 和当前的 props 返回我们需要的数据

高价组件示例:

// 此函数接收一个组件(传入组件)...
function withSubscription(WrappedComponent, selectData) {
  // ...并返回另一个组件(增强组件)...
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props),
      };
    }

    componentDidMount() {
      // ...负责订阅相关的操作...
      DataSource.addChangeListener(this.handleChange);
    }

    componentWillUnmount() {
      DataSource.removeChangeListener(this.handleChange);
    }

    handleChange() {
      this.setState({
        data: selectData(DataSource, this.props),
      });
    }

    render() {
      // ... 并使用新数据渲染被包装的组件!
      // 请注意,我们可能还会传递其他属性
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}

HOC 特性:

  • HOC 接收一个组件(传入组件),使用数据将该组件组合成一个新组件,并返回新组件(增强组件)
  • HOC 不会修改传入的组件,只是添加了某些属性
  • HOC 应该是纯函数,不能有副作用

HOC 错误示例:❌

HOC 中修改组件原型:

function logProps(InputComponent) {
  InputComponent.prototype.componentDidUpdate = function (prevProps) {
    console.log("Current props: ", this.props);
    console.log("Previous props: ", prevProps);
  };
  // 返回原始的 input 组件,暗示它已经被修改。
  return InputComponent;
}

// 每次调用 logProps 时,生命周期函数componentDidUpdate都会执行,增强组件都会有 log 输出。
const EnhancedComponent = logProps(InputComponent);

2. 高阶组件有何用?

定义中已经说了,高阶组件是为了解决 React 组件的组合问题

高阶组件主要解决关注点横切的问题

什么是关注点横切

通俗的说就是,某些任务(关注点)被多个模块重复使用(横切)。

常见的关注点横切有:

  • 日志功能:多个模块可能都会用到
  • 获取用户信息

一句话终结就是:HOC 可以帮助我们抽象出组件的公共部分

3. 高阶组件和普通组件的区别

高阶组件和普通组件的区别是

普通组件将 props/state 转换为 UI,而高阶组件将组件转换为另一个组件

上面这句话来自React-高阶组件官方文档,它具有纲领意义。

如果我们需要处理东西和 UI 相关,那么,他就是组件的组合问题

如果我们处理的东西是多个组件共同所有的,想要把共有的处理办法提取出来,那么就需要使用 HOC。

4. 如何实现高阶组件

高阶组件就是要操作 WrappedComponent,得到一个增强的新组件。

如何操作呢 ❓

  • 属性代理(props proxy):高阶组件通过被包裹的 React 组件来操作 props
  • 反向继承(inheritance inversion):高阶组件继承于被包裹的组件

3.1 属性代理

Props Proxy 的简单实现

function HOC(WrappedComponent) {
  return class EnhancedComponent extends React.Component {
    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
}

需要说明的是,EnhancedComponent 在这里是容器组件,它包裹着包裹组件(WrappedComponent),🌟 就是它来处理具体增强逻辑

HOC 做了什么?

通过给 WrappedComponent 传递 this.props 得到一个新的组件

this.propsWrappedComponent本身具有的属性,使用EnhancedComponent时可以直接使用它们。

示例 1:

import { Component } from "react";
// 定义被包裹组件
const Mouse = (props) => {
  return (
    <span className="mouse" role={props.role}>
      🐭
    </span>
  );
};

// 定义 HOC
const withDrag = (Wrapped) => {
  class WithDrag extends Component {
    render = () => {
      // this.props是要透传给 Wrapped 的属性
      return <Wrapped {...this.props} />;
    };
  }
  return WithDrag;
};

// 生成增强组件 MouseDraggable
const MouseDraggable = withDrag(Mouse);

// 使用增强组件
ReactDOM.render(
  <MouseDraggable role="mouse" />,
  document.getElementById("root")
);

这是个基础示例,并没有增强Mouse 组件的任何功能。

我们一起看看属性代理可以做什么:

  • 操作 props
  • 通过 Refs 访问到组件实例
  • 提取 state
  • 用其他元素包裹 WrappedComponent

3.1.1 操作 props

可以读取、添加、编辑、删除传给 WrappedComponent 的 props

改进例 1 中的 HOC:

const withDrag = (Wrapped) => {
  class WithDrag extends Component {
    // 增加新的 props
    const newProps = {
      title: "draggable"
    }
    render = () => {
      return <Wrapped {...this.props} {...newProps} />;
    };
  }
  return WithDrag;
};

3.1.2 通过 Refs 访问到组件实例

refkey一样,它不是 prop 属性,会被 React 进行特殊处理。

如果对 HOC 添加 ref,该 ref 将引用最外层的容器组件,而不是被包裹的组件

...
const withDrag = (Wrapped) => {
  class WithDrag extends Component {
    render = () => {
      return <Wrapped {...this.props} />;
    };
  }
  return WithDrag;
};

const MouseDraggable = withDrag(Mouse);
// 创建ref
const ref = React.createRef();

ReactDOM.render(
  <MouseDraggable role="mouse" ref={ref} />,
  document.getElementById("root")
);

这里的 ref 指向 HOC 的容器组件 -- WithDrag。

React.forwardRef 就是一个高阶组件,它能帮我们转发 ref 到增强后的组件。

import { Component } from "react";
const Mouse = (props) => {
  return (
    <span className="mouse" role={props.role}>
      🐭
    </span>
  );
};

const withDrag = (Wrapped) => {
  class WithDrag extends Component {
    render = () => {
      return <Wrapped {...this.props} />;
    };
  }
  return WithDrag;
};

const MouseDraggable = withDrag(Mouse);
const ref = React.createRef();

ReactDOM.render(
  <MouseDraggable role="mouse" ref={ref} />,
  document.getElementById("root")
);

这里的 ref 指向 MouseDraggable

3.2 反向继承

4. 约定

既然高阶组件是一种设计模式,我们应该准守它的设计约定。

5. 注意事项

参考

Blog:

浅显易懂,动画辅助 -- REACT HIGHER ORDER COMPONENTS IN 3 MINUTES

深入讲解,值得研究 -- React Higher Order Components in depth

文档:

高阶组件

暂时看看,翻译

参考

官方文档,总结全面,但不太容易理解 -- 高阶组件

生动形象 -- REACT HIGHER ORDER COMPONENTS IN 3 MINUTES

Stay Tuned

Want to become a Next.js pro?
The best articles, links and news related to web development delivered once a week to your inbox.