React里的高阶组件HOC

概念

  1. Higher-Order Function,是函数式编程里的一个基本概念,接受函数作为输出,或者输出一个函数。
  2. 函数式编程推崇每个函数只执行一个功能,整体通过函数组合起来,Ramda.js从右向左的思想很地道。
  3. reduce、map等,都是一种高阶函数
  4. React里的高阶组件Higher-Order Components也是如此,它接受一个React组件作为输入,输出一个新的React组件。如同用一个容器进行包裹,返回一个增强的组件,使得代码更加有抽象性,具有复用性,可以对props、state进行控制,也可以对render进行劫持
  5. 正如组件把props -> ui,高阶组件做的是把组件 -> 另一个组件。看起来就像是这样const EnhancedComponent = higherOrderComponent(WrappedComponent);
  6. 高阶组件应该是纯函数,是没有side-effects的。HOC本身不关心数据会怎么被使用,而被包裹的组件不关心数据从何而来。高阶组件和被包裹的组件之间,就是完全props-based。可以把HOC想像成带参数的容器组件(容器组件管理订阅和状态,并且以props的形式传入UI组件)。
  7. Redux的connectRelay里的createFragmentConttainer也都是高阶函数

    1
    2
    3
    4
    5
    6
    7
    8
    // React Redux's connect
    const ConnectedComment = connect(commentSelector, commentActions)(CommentList);

    // 其实大概是这么一个机制
    // connect is a function that returns another function
    const enhance = connect(commentListSelector, commentListActions);
    // The returned function is a HOC, which returns a component that is connected to the Redux store
    const ConnectedComment = enhance(CommentList);

使用原则

不要修改被包裹组件的prototpye

  1. 通过WrappedComponent.prototype,是可以访问到如componentWillReceiveProps的。
  2. 但是不应该这么做,如果希望在生命周期钩子上做一些事,应该在HOC自己的对应的生命周期上去做,能达到同样的效果
  3. 不修改WrappedComponent.prototype,才不至于不恰当地影响它;而且这样即使多次套HOC,也不会有冲突;也符合分离的原则,不至于写出有毒的代码。

经过HOC包装后,返回的组件应该和WrappedComponent有相似的接口

  1. 这么做的目的是提升组件们的可复用性
  2. 额外的props应该被分离传入,比如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    render() {
    // Filter out extra props that are specific to this HOC and shouldn't be
    // passed through
    const { extraProp, ...passThroughProps } = this.props;

    // Inject props into the wrapped component. These are usually state values or
    // instance methods.
    const injectedProp = someStateOrInstanceMethod;

    // Pass props to wrapped component
    return (
    <WrappedComponent
    injectedProp={injectedProp}
    {...passThroughProps}
    />
    );
    }

设置合适的displayName方便debug

  1. displayName就是用于debugging message的,一般来说是不需要特别设置的,因为他会根据组件的类名有一个默认值
  2. 但是如果使用了高阶组件,合理的设置displayName,就能让调试过程更加简单和顺利(目的是能清楚的区分组件是高阶组件还是一般的组件)。
  3. 官方文档推荐的做法是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function withSubscription(WrappedComponent) {
    class WithSubscription extends React.Component {/* ... */}
    WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
    return WithSubscription;
    }

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

应用场景

属性代理:通过被包裹的组件来操作Props

  1. 通过高阶函数的装饰,生命周期是:didmount -> HOC didmount -> (HOCs didmount) -> … -> (HOCs willunmount)-> HOC willunmount -> unmount
  2. 既然是包裹,很容易想到,可以随意增加外层,来添加样式、方便布局等,比如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import React, { Component } from 'react'

    const ExampleHoc = WrappedComponent => {
    return class extends Component {
    render() {
    return (
    <div style={{display: 'flex'}}>
    <WrappedComponent {...this.props} />
    </div>
    )
    }
    }
    }
    export default ExampleHoc
  3. 可以读取、编辑、增加、移除从WrappedComponent传来的props,需要注意,应该对高阶函数的props作新的命名,防止混淆。比如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import React, { Component } from 'react'

    const ExampleHoc = WrappedComponent => {
    return class extends Component {
    render() {
    const newProps = {
    name: newText,
    }
    return <WrappedComponent {...this.props} {...newProps}/>
    }
    }
    }
    export default ExampleHoc
  4. 可以通过WrappedComponent提供props和回调函数抽象state,比如把原组件抽象成展示型组件,分离内部状态,弄成无状态组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    import React, { Component } from 'react';

    const ExampleHoc = WrappedComponent => {
    return class extends Component {
    constructor(props) {
    super(props)
    this.state = {
    name: '',
    }
    }
    onNameChange = e => {
    this.setState({
    name: e.target.value,
    })
    }
    render() {
    const newProps = {
    name: {
    value: this.state.name,
    onChange: this.onNameChange,
    }
    }
    return <WrappedComponent {...this.props} {...newProps} />
    }
    }
    }
    export default ExampleHoc

反向继承:高阶组件继承于被包裹的组件

  1. 原理就是让高阶函数返回的组件继承于被包裹的组件WrappedComponent,和属性代理不同,方法可以通过super来顺序调用,此时的生命周期是:didmount -> HOC didmount ->(HOCs didmount) -> willunmount -> HOC willunmount ->(HOCs willunmount)
  2. 这种结构,使得高阶函数可以使用WrappedComponent的state、props、生命周期和render方法!
  3. 渲染劫持,高阶组件控制WrappedComponent的渲染过程,但是如果元素树里有函数类型的React组件,就不能操作组件的子组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const ExampleHoc = WrappedComponent => {
    return class extends WrappedComponent {
    render() {
    const eleTree = super.render()
    let newProps = {}

    if(eleTree && eleTree.type === 'input') {
    newProps = {value: '这不能被渲染'}
    }
    const props = Object.assgin({}, eleTree.props, newProps)
    const newEleTree = React.cloneElement(eleTree, props, eleTree.props.children)
    return newEleTree
    }
    }
    }
  4. 虽然也可以读取、修改、删除WrappedComponent的state,但是应该尽量减少,因为会增加混乱,或者至少要重新命名state

高阶函数接受其他参数

  1. 代码上增加一层即可

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import React, { Component } from 'react'

    const ExampleHoc = (key) => (WrappedComponent) => {

    return class extends Component {
    render() {
    return <WrappedComponent {...this.props} />
    }
    }
    }
  2. 又比如,直接接受复数的参数,第二个参数可以传入一个函数,其返回值就是关心的数据,由此我们可以随意传入更多的参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    // 使用
    const BlogPostWithSubscription = withSubscription(
    BlogPost,
    (DataSource, props) => DataSource.getBlogPost(props.id)
    );
    // 定义
    // This function takes a component...
    function withSubscription(WrappedComponent, selectData) {
    // ...and returns another component...
    return class extends React.Component {
    constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
    data: selectData(DataSource, props)
    };
    }
    componentDidMount() {
    // ... that takes care of the subscription...
    DataSource.addChangeListener(this.handleChange);
    }
    componentWillUnmount() {
    DataSource.removeChangeListener(this.handleChange);
    }
    handleChange() {
    this.setState({
    data: selectData(DataSource, this.props)
    });
    }
    render() {
    // ... and renders the wrapped component with the fresh data!
    // Notice that we pass through any additional props
    return <WrappedComponent data={this.state.data} {...this.props} />;
    }
    };
    }