React 高阶组件

386 阅读7分钟

React文档:高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。 HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。 总结来说就是两点:

什么是高阶组件

官方说,高阶组件是参数为组件,返回值为新组件的函数。

const EnhancedComponent = higherOrderComponent(WrappedComponent);

通俗的定义:高阶组件是函数,通过包裹(wrapped)被传入的React组件,经过一系列处理,最终返回一个相对增强(enhanced)的React组件,供其他组件调用。

另一个相似概念:高阶函数

Higher-Order 这个单词相信都很熟悉,Higher-Order function(高阶函数)在函数式编程是一个基本概念,它描述的是这样一种函数,接受函数作为输入,或是输出一个函数。比如常用的工具方法 map、reduce、sort 都是高阶函数。

为什么要用高阶组件

React 的文档说的非常清楚, 高阶组件是一种用于复用组件逻辑模式。总结来说就是两点:

  • 逻辑复用. 把一些通用的代码逻辑提取出来放到高阶组件中, 让更多组件可以共享
  • 分离关注点. 在之前的章节中提到"逻辑和视图分离"的原则. 高阶组件可以作为实现该原则的载体. 我们一般将行为层或者业务层抽取到高阶组件中来实现, 让展示组件只关注于 UI

与父组件区别

高阶组件作为一个函数,它可以更加纯粹地关注业务逻辑层面的代码,比如数据处理,数据校验,发送请求等,可以改善目前代码里业务逻辑和UI逻辑混杂在一起的现状。父组件则是UI层的东西,我们先前经常把一些业务逻辑处理放在父组件里,这样会造成父组件混乱的情况。为了代码进一步解耦,可以考虑使用高阶组件这种模式。

高阶组件的用法

  • 属性代理(Props Proxy): 代理传递给被包装组件的 props, 对 props 进行操作. 这种方式用得最多. 使用这种方式可以做到:

    • 操作 props
    • 通过refs访问被包装组件实例
    • 提取 state
    • 用其他元素包裹WrappedComponent
  • 反向继承(Inheritance Inversion): 高阶组件继承被包装的组件. 可以实现:

    • 渲染劫持: 即控制被包装组件的渲染输出.
    • 操作 state: state 一般属于组件的内部细节, 通过继承的方式可以暴露给子类. 可以增删查改被包装组件的 state, 除非你知道你在干什么, 一般不建议这么做.

属性代理例子:

export default function withHeader(WrappedComponent) {
  return class HOC extends Component {
    render() {
      const newProps = {
        test:'hoc'
      }
      // 透传props,并且传递新的newProps
      return <div>
        <WrappedComponent {...this.props} {...newProps}/>
      </div>
    }
  }
}

反向继承例子:

export default function (WrappedComponent) {
  return class Inheritance extends WrappedComponent {
    componentDidMount() {
      // 可以方便地得到state,做一些更深入的修改。
      console.log(this.state);
    }
    render() {
      return super.render();
    }
  }
}

总结一下高阶组件的应用场景:

  • 操作 props: 增删查改 props. 例如转换 props, 扩展 props, 固定 props, 重命名 props
  • 依赖注入. 注入 context 或外部状态和逻辑, 例如 redux 的 connnect, react-router 的 withRouter. 旧 context 是实验性 API, 所以很多库都不会将 context 保留出来, 而是通过高阶组件形式进行注入
  • 扩展 state: 例如给函数式组件注入状态
  • 避免重复渲染: 例如 React.memo
  • 分离逻辑, 让组件保持 dumb

高阶组件的示例

简而言之,如果你需要给很多组件都写相同的判断逻辑,那么可以考虑提取出一个高阶组件

实现一个高阶组件

下面我们来实现一个最简单的高阶组件(函数),它接受一个React组件,包裹后然后返回。

export default function withHeader(WrappedComponent) {
  return class HOC extends Component {
    render() {
      return <div>
        <div className="demo-header">
          我是标题
        </div>
        <WrappedComponent {...this.props}/>
      </div>
    }
  }
}

在其他组件里,我们引用这个高阶组件,用来强化它。

@withHeader
export default class Demo extends Component {
  render() {
    return (
      <div>
        我是一个普通组件
      </div>
    );
  }
}

在这里使用了ES7里的decorator,来提升写法上的优雅,但是实际上它只是一个语法糖,下面这种写法也是可以的。

const EnhanceDemo = withHeader(Demo);

组合多个高阶组件

上述高阶组件为React组件增强了一个功能,如果需要同时增加多个功能需要怎么做?这种场景非常常见,例如我既需要增加一个组件标题,又需要在此组件未加载完成时显示Loading。

@withHeader
@withLoading
class Demo extends Component{

}

使用compose可以简化上述过程,也能体现函数式编程的思想。

const enhance = compose(withHeader,withLoading);
@enhance
class Demo extends Component{

}

调试规范

但是随之带来的问题是,如果这个高阶组件被使用了多次,那么在调试的时候,会显示在 React Developer Tools 中,将会看到一大堆HOC,所以这个时候需要做一点小优化,就是在高阶组件包裹后,应当保留其原有名称。

我们改写一下上述的高阶组件代码,增加了getDisplayName函数以及静态属性displayName,此时再去观察DOM Tree。

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

function getDisplayName(component) {
  return component.displayName || component.name || 'Component';
}

function withHeader(WrappedComponent) {
  return class HOC extends React.Component {
    static displayName = `HOC(${getDisplayName(WrappedComponent)})`
    render() {
      return <div>
        <div className="demo-header">我是标题</div>
        <WrappedComponent {...this.props}/>
      </div>
    }
  }
}


class Demo extends React.Component {
  render() {
    return (
      <div>我是一个普通组件</div>
    );
  }
}
const EnhanceDemo = withHeader(Demo)

class MyComponent extends React.Component{
  render(){
    return <EnhanceDemo/>
  }
}

ReactDOM.render(
  <MyComponent />,
  document.getElementById('root')
);

此时,原本组件的名称正确地显示在了DOM Tree上。

MyComponent
  HOC(Demo)
    <div>
      <div className="demo-header">我是标题</div>
      <Demo>
        <div>我是一个普通组件</div>
      </Demo>
  HOC(Demo)
MyComponent
        

由此可以看出,高阶组件的主要功能是封装并抽离组件的通用逻辑,让此部分逻辑在组件间更好地被复用。

高阶组件实战

实现一个loading组件

实现Loading组件时,发现需要去拦截它的渲染过程,故使用了反向继承的方式来完成。

在通过装饰器调用时,需要传入一个函数作为入参,函数可以获取到props,随后返回一个Boolean对象,来决定组件是否需要显示Loading态

import React, {Component} from 'react';
import {Spin} from 'antd';
export default function (loadingCheck) {
  return function (WrappedComponent) {
    return class extends WrappedComponent {
      componentWillUpdate(nextProps, nextState) {
        console.log('withLoading将会更新');
      }
      render() {
        if (loadingCheck(this.props)) {
          return <Spin tip="加载中" size="large">
            {super.render()}
          </Spin>
        } else {
          return super.render();
        }
      }
    }
  }
}

// 使用
@withLoading(props => {
  return props.IndexStore.accountList.length == 0;
})

实现一个copy组件

实现copy组件的时候,我们发现不需要去改变组件内部的展示方式,只是为其在外围增加一个功能,并不会侵入被传入的组件,故使用了属性代理的方式。

import gotem from 'gotem';
import React, {Component} from 'react';
import ReactDom from 'react-dom';
import {message} from 'antd';
export default copy = (targetName) => {
  return (WrappedComponent) => {
    return class extends Component {
      componentDidMount() {
        const ctx = this;
        const dom = ReactDom.findDOMNode(ctx);
        const nodes = {
          trigger: dom,
          // targetName为DOM选择器,复制组件将会复制它的值
          target: dom.querySelector(targetName)
        };
        // 当trigger被点击时就会把target下文本内容拷贝到剪切板上
        gotem(nodes.trigger, nodes.target, {
          success: function () {
            message.success('复制成功');
          },
          error: function () {
            message.error('复制失败,请手动输入');
          }
        });
      }
      render() {
        return <WrappedComponent {...this.props}/>;
      }
    };
  };
}
// 使用
// 传入 h3 ,让复制组件去获取它的值
@copy('h3')
class Info extends Component {
  render() {
    return (
      <div>
        <h3>
          阿里云,点击复制这段文字
        </h3>
      </div>
    );
  }
}

总结

高阶组件是Decorator模式在React的一种实现,它可以抽离公共逻辑,像洋葱一样层层叠加给组件,每一层职能分明,可以方便地抽离与增添。在优化代码或解耦组件时,可以考虑使用高阶组件模式。

参考文章

官网高阶组件
React 高阶组件
深入理解 React 高阶组件
React组件设计实践总结04 - 组件的思维
关于React的高阶组件
React高阶组件总结