React HOC 高阶组件

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

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

实现方式

属性代理

返回用函数包裹原组件生成新组件,可以看做是被代理组件的父组件:

// proxyHOC.jsx
// hoc
const proxyHOC = (WrappedComponent) {
  // 继承的是 React.Component
  return class extends Component {
    render() {
      return <WrappedComponent {...this.props} />;
    }
  }
}
// TestComponent.jsx
// 原组件
class TestComponent extends Component{
    ...
}
// 生成新组件    
const TestWithHoc = proxyHOC(TestComponent);
// 将生成的新组件暴露出去
export default TestWithHoc

反向继承

返回一个继承原组件的新组件,因为是直接继承,所以可以访问到原组件的生命周期、props、state 以及 render 函数:

const inheritHOC = (WrappedComponent) {
  // 继承的是原组件
  return class extends WrappedComponent {
    render() {
      // super 关键字用于访问和调用一个对象的父对象上的函数
      return super.render();
    }
  }
}

实现了什么功能

操作 props

在 HOC 内部可以对传递过来的 props 进行修改,再传给新组件:

const proxyHOC = (WrappedComponent) {
  return class extends Component {
    render() {
      // 对 props 进行改造
      const newProps = {
          ...this.props,
          name:'Test'
      }
      return <WrappedComponent {...this.newProps} />;
    }
  }
}

获得 refs 的引用

在 HOC 内部,可以获取到新组件的 ref:

import React,{ Component, createRef } from 'react';

const proxyHOC = (WrappedComponent) => {
    return class extends Component{    
        constructor(props) {
            super(props);
            this.ref = React.createRef();
        }
        
        render(){
            return (
                <WrappedComponent {...this.props} ref={this.ref} />
            )
        }
    }
}

传递 ref 属性

ref 属性不属于 props,无法通过 props 向下传递(这里需要注意,不是 React.createRef 创造的值不能被传递,是 ref 这个属性无法被传递),举个例子:

// Parent.jsx
class Parent extends Component {
  constructor(props) {
    super(props);
    this.ref = React.createRef();
  }

  render() {
    return <Child ref={this.ref} data={'abc'}></Child>;
  }
}
// Child.jsx
class Child extends Component {
  constructor(props) {
    super(props);
  }

  render() {
    return <div>{ console.log(this.props) }</div>;
  }
}

// output: { data:abc }

可以看到,并没有打印出 ref,可见 ref 并没有通过 props 被传递下去,因为 ref 不是 prop 属性,就像 key 一样,被 React 做了特殊的处理(不论是 function Component 还是 Class Component)。

而且,当 ref 指向高阶组件生成的新组件时,ref 其实指向的是高阶组件,举个例子:

// childplay.js
const ChildPlay = play(Child); 
export default ChildPlay;

// another.js
<ChildPlay ref={this.ref}>

上面返回了 ChildPlay 组件,当我们对其 ref 赋值的时候,ref 实际上是指向了 play 而不是内部的 Child 组件,这意味着不能使用 ref.current.playMusic()这样的方法。

解决这个问题,可以使用forwardRef明确地将 ref 转发到内部的 Child 组件中去:

// Child.jsx
class Child extends Component {
  constructor(props) {
    super(props);
  }

  render() {
    return <div className={"child"}>{console.log(this.props)}</div>;
  }
}

const ChildPlay = play(Child); // 用hoc包裹Child组件

export default ChildPlay;
// playHoc.jsx
export const play = (WrappedComponent) => {
    class InnerPlay extends Component {
        render() {
            const {forwardedRef,...props} = this.props;
            return <WrappedComponent ref={forwardedRef} {...props} />;
        }
    }

    return React.forwardRef((props,ref)=>{
        // forwardRef方法获取的就是传递过来的ref属性
        return <InnerPlay forwardedRef={ref} {...props}/>
    })
};

上面高阶函数和最基础的高阶函数区别在于:

他不直接返回类组件,而是先创建一个内部的组件(InnerPlay);

然后调用React.forwardRef(),并在该方法中,将父组件(Parent)传递过来的 ref 以 props 的形式传递给 InnerPlay;

再将 ref 通过 forwardedRef 赋值给 InnerPlay 组件的 props ;

最后 InnerPlay 组件将 forwardedRef 取出赋给目标组件的 ref 属性;

经过这样的操作,Parent 就能通过 ref 获取到 Child 组件了。

抽象state

即:将不受控组件转换为受控组件,举个例子:

class WrappedComponent extends Component {
    render() {
        return <input name="name" {...this.props.name} />;
    }
}

const proxyHOC = (WrappedComponent) => {
    return class extends Component {
        constructor(props) {
            super(props);
            this.state = {
                name: '',
            };
            this.onNameChange = this.onNameChange.bind(this);
        }

        onNameChange(event) {
            this.setState({
                name: event.target.value,
            })
        }

        render() {
            const newProps = {
                name: {
                    value: this.state.name,
                    onChange: this.onNameChange,
                },
            }
            return <WrappedComponent {...this.props} {...newProps} />;
        }
    }    
}

用其他元素包装组件

在 render 中可以在组件外添加额外的元素,常用来增加布局和修改样式:

const proxyHOC = (WrappedComponent) =>{
    return class extends Component{
        render(){
            return (
                <div>
                   <WrapperComponent {...this.props}></WrapperComponent>
                </div>
            )
        }
    }
}

渲染劫持

举个例子:

function withAuth(WrappedComponent) {
    return class extends WrappedComponent {
        render() {
            if (this.props.loggedIn) {
                return super.render(); 
            } else {
                return null; 
            } 
        } 
    } 
}

解决什么实际问题

逻辑复用

需要被复用的逻辑形成高阶函数,包裹目标组件,解决代码复用问题。例如常见的日志记录:

const logHoc = (WrappedComponent) {
  return class extends Component {
    
    componentDidCatch(error,info){
        // 对错误做出响应
    }
      
    render() {
      return <WrappedComponent {...this.props} />
    }
  }
}

权限控制

根据用户的权限,显示或隐藏具体内容

const auth = (WrappedComponent) {
  return class extends Component {
    render() {
      // 判断用户是否具有权限
      if( !hasPermission ) return null;
      return <WrappedComponent {...props} />;
    }
  }
}

注意

  • HOC 不会也不应该去修改组件,它会生成一个新的组件,对原组件没有副作用;

  • 不要在 render 方法中调用 HOC,因为每次调用 render 时都会生成一个新的组件,会在 diff 的时候被销毁:

    render() {
        // 每次调用 render 函数都会创建一个新的 EnhancedComponent
        const EnhancedComponent = enhance(MyComponent);
        // 这将导致子树每次渲染都会进行卸载,和重新挂载的操作!
        return <EnhancedComponent />;
    }
    
  • HOC 不会复制组件的静态方法,需要手动拷贝,举个例子:

    // 定义静态函数
    WrappedComponent.staticMethod = function() {/*...*/}
    // 现在使用 HOC
    const EnhancedComponent = enhance(WrappedComponent);
    
    // 增强组件没有 staticMethod
    typeof EnhancedComponent.staticMethod === 'undefined' // true
    

    hoist-non-react-statics 库能够帮助我们批量拷贝静态方法。

  • 为了在开发与调试时更好地区分 HOC,推荐设置 HOC 的显示名称:

    const HOC = (WrappedComponent) =>{
        return class extends Component{
            static displayName = `HOC(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`;
            
            render(){
                return <WrapperComponent {...this.props} data={'ABC'}></WrapperComponent>
            }
        }
    }
    

传参

HOC 可以传递组件和其他参数,常用的是 HOC(…params)(WrappedComponent)的形式,Redux 的 connect 、AntDesign 的 Form.Create 采用的都是这种形式:

// connect 是一个函数,它的返回值为另外一个函数。
const enhance = connect(selector, actions);
// 返回值为 HOC,它会返回已经连接 Redux store 的组件
const ConnectedComment = enhance(CommentList);

要使用此形式,需要将 HOC 稍微改写一下:

function HOC = (...params) => (WrappedCompoennt) =>{
    ...
}
作者

BiteByte

发布于

2020-08-08

更新于

2024-11-15

许可协议