React高阶组件(HOC)入门及关于ref

2,409 阅读7分钟

高阶组件的基本概念(是什么❓)

  • 高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。
  • 具体而言,高阶组件是参数为组件,不是函数,返回值为新组件的函数(在此之前可以先了解一下高阶函数)。
const EnhancedComponent = higherOrderComponent(WrappedComponent);
  • 区别组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件。
  • 概念比较简单,实际也好用。HOC 在 React 的第三方库中很常见,例如 Redux 的 connect 和 Relay 的 createFragmentContainer。

使用高阶组件的原因(为什么❓)

  • 学习新的技术无非要么就是装杯💪💪,要么就是对于项目中带来了实际作用,并且能提升开发效率。(两者兼有)
  • 关于高阶组件能解决的问题可以简单概括成以下三个方面:
    • 抽取重复代码,实现组件复用,常见场景:页面复用。
    • 条件渲染,控制组件的渲染逻辑(渲染劫持),常见场景:权限控制。
    • 捕获/劫持被处理组件的生命周期,常见场景:组件渲染性能追踪、日志打点
  • 下面就来介绍实现方式,从而加深大家对高阶组件作用的理解。

高阶组件的实现(怎么做❓)

  • 通常情况下,实现高阶组件的方式有以下两种:
    • 属性代理(Props Proxy)
      • 返回一个无状态(stateless)的函数组件
      • 返回一个 class 组件
    • 反向继承(Inheritance Inversion)

高阶组件实现方式的差异性决定了它们各自的应用场景:一个 React 组件包含了 props、state、ref、生命周期方法、static方法和React 元素树几个重要部分,所以我将从以下几个方面对比两种高阶组件实现方式的差异性:

  • 原组件是否被继承
  • 能否读取/操作原组件的 props
  • 能否读取/操作原组件的 state
  • 能否通过 ref 访问到原组件的 dom 元素
  • 是否影响原组件某些生命周期等方法
  • 是否取到原组件 static 方法
  • 能否劫持原组件生命周期方法
  • 能否渲染劫持

属性代理

  • 该方式实现的高阶组件会影响到元组件

操作props

  • 示例:
import React from 'react';
function MyHoc(WrapperComponent) {
  return class LogoProps extends React.Component {
    render() {
      const newProps = { type: 'logoProps'}
      return (
        <WrapperComponent {...this.props} {...newProps} />
      )
    }
  }
}
export default MyHoc;
  • 从上面代码可看出我们除了可以拦截到父组件的props,还可以对该props进行操作,例如增加一个type属性

抽象state

  • 本质上来说高阶组件内是不能操作原组件state,但是非要这么干,就通过props回调的方式是可以实现的
  • 来看看吧:
//MyHoc.js
import React from 'react';
function MyHoc(WrapperComponent) {
  return class LogoProps extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        value: ''
      }
    }
    onChange = (e) => {
      this.setState({
        value: e.target.value
      })
    }
    render() {
      const newProps = {
        name: {
          value: this.state.value,
          onChange: this.onChange
        }
      }
      return (
        <WrapperComponent {...this.props} {...newProps} />
      )
    }
  }
}
export default MyHoc;
//FancyComponent.js
import React from 'react';
import MyHoc from './MyHoc';

class FancyComponent extends React.Component{
  render() {
    return (
      <input {...this.props.name} />
    )
  }
}

export default MyHoc(FancyComponent);

获取静态方法

直接看栗子,比较简单:

MyHoc.js
import React from 'react';

function MyHoc(WrapperComponent) {
  return class LogoProps extends React.Component {
    componentDidMount() {
      // 获取父组件静态方法
      WrapperComponent.sayHello()
    }
    render() {
      return (
        <WrapperComponent {...this.props}  />
      )
    }
  }
}

export default MyHoc;
//FancyComponent.js
import React from 'react';
import MyHoc from './MyHoc';
import ReverseHoc from './ReverseHoc';

class FancyComponent extends React.Component{
  constructor(props) {
    super(props);
    this.state = {

    }
  }
  static sayHello() {
    console.log('say hello')
  }
  render() {
    return (
      <input {...this.props.name} />
    )
  }
}

export default MyHoc(FancyComponent);

输出:say hello

反向代理

反向继承指的是使用一个函数接受一个组件作为参数传入,并返回一个继承了该传入组件的类组件,且在返回组件的 render() 方法中返回 super.render() 方法,最简单的实现如下:

const HOC = (WrappedComponent) => {
  return class extends WrappedComponent {
    render() {
      return super.render();
    }
  }
}
  • 与属性代理的区别是:使用反向继承方式实现的高阶组件的特点是允许高阶组件通过 this 访问到原组件,所以可以直接读取和操作原组件的 state/ref/生命周期方法。

劫持原组件生命周期

  • 高阶组件会覆盖传入组件的生命周期,示例如下:
//ReverseHoc.js
import React from 'react';
function ReverseHoc(WrapperComponent) {
  return class LogoProps extends WrapperComponent {
    componentDidMount() {
      console.log('this is ReverseHoc componentDidMount Life');
    }
    render() {
     super.render()
    }
  }
}
export default ReverseHoc;
//FancyComponent.js
import React from 'react';
import MyHoc from './MyHoc';
import ReverseHoc from './ReverseHoc';

class FancyComponent extends React.Component{
  constructor(props) {
    super(props);
    this.state = {

    }
  }
  componentDidMount() {
    console.log('this is FancyComponent componentDidMount Life')
  }
  render() {
    return (
      <input />
    )
  }
}

// export default MyHoc(FancyComponent);
export default ReverseHoc(FancyComponent);

输出:

'this is ReverseHoc componentDidMount Life'
  • 假如我不想覆盖,当传入组件有该生命周期,高阶组件就不覆盖,无则覆盖传入组件生命周期。 我们改造一下ReverseHoc.js:
//ReverseHoc.js
import React from 'react';
function ReverseHoc(WrapperComponent) {
  const DidMount = WrapperComponent.prototype.componentDidMount;
  return class LogoProps extends WrapperComponent {
     async componentDidMount() {
      if(DidMount) {
         await DidMount.apply(this)
      }
      console.log('this is ReverseHoc componentDidMount Life');
    }
    render() {
     super.render()
    }
  }
}
export default ReverseHoc;

这样做就可以实现上面说的效果啦!

操作/读取state

  • 因为我们高阶组件继承了传入组件,那么就是能访问到this了,有了this是不是就能操作和读取state,也就不用像属性代理那么复杂还要通过props回调来操作state
  • 示例:
//ReverseHoc.js
import React from 'react';
function ReverseHoc(WrapperComponent) {
  return class LogoProps extends WrapperComponent {
     async componentDidMount() {
       //读取state
      console.log(this.state)
      //操作state
      this.setState({ type: 1 });
    }
    render() {
     super.render()
    }
  }
}

export default ReverseHoc;

注意事项👉(官方文档):

  • 不要在 render 方法中使用 HOC
  • 务必复制静态方法
  • Refs 不会被传递(下面有详细介绍)

Refs转发

  • 基本概念:Ref 转发是一项将 ref 自动地通过组件传递到其一子组件的技巧。对于大多数应用中的组件来说,这通常不是必需的。但其对某些组件,尤其是可重用的组件库是很有用的。(应用于非受控组件)

过时API:String类型的Refs(官方不建议使用)

示例:

import React from 'react';

export default class UnControlledComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      submitVal: '',
    }
  }

  handleSubmit = (e) => {
    e.preventDefault()
    // this.refs.inputRef.value 得到输入框输入的内容
    this.setState({
       submitVal: this.refs.inputRef.value
    })
  }
  render() {
    const { submitVal } = this.state;
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" ref="inputRef" />
        </label>
        <input type="submit" value="Submit" />
        <h4>提交内容:{submitVal}</h4>
      </form>
    )
  }
}

Refs回调

示例:

import React from 'react';

export default class UnControlledComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      submitVal: '',
    }
    this.inputRef = React.createRef();
  }

  handleSubmit = (e) => {
    e.preventDefault()
    // this.inputRef.value 得到输入框输入的内容
     this.setState({
      submitVal: this.inputRef.value
    })
  }
  render() {
    const { submitVal } = this.state;
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" ref={(ref) => this.inputRef = ref} />
        </label>
        <input type="submit" value="Submit" />
        <h4>提交内容:{submitVal}</h4>
      </form>
    )
  }
}

createRef方式

import React from 'react';

export default class UnControlledComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      inputVal: '',
      submitVal: '',
    }
    this.inputRef = React.createRef();
  }

  handleSubmit = (e) => {
    e.preventDefault()
    // this.inputRef.current.value 获取输入框输入内容
     this.setState({
       submitVal: this.inputRef.current.value
     })
  }
  render() {
    const { submitVal } = this.state;
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" ref={this.inputRef} />
        </label>
        <input type="submit" value="Submit" />
        <h4>提交内容:{submitVal}</h4>
      </form>
    )
  }
}

在高阶组件中转发 refs

首先我们看个示例:

// MyHoc.js
import React from 'react';

const MyHoc = (WrapperComponent) => {
  return class LogoProps extends React.Component {
    render() {
      const { forwardRef, ...rest } = this.props;
      return (
        <WrapperComponent {...rest} ref={forwardRef} />
      )
    }
  }
}

export default MyHoc;
// FancyInput.js
import MyHoc from './MyHoc';
import React from 'react';


 class FancyInput extends React.Component {
   render() {
     return (
       <input />
     )
   }
 }
export default MyHoc(FancyInput)
//App.js
import React from 'react';
import './App.css';
import FancyInput from './RefsOfHOC/FancyInput';

class App extends React.Component {
  constructor(props) {
    super(props);
    this.forWardRef = React.createRef()
  }
  componentDidMount() {
    console.log(this.forWardRef)
  }
  render() {
    return (
      <div className="App">
        <FancyInput ref={this.forWardRef} />
      </div>
    )
  }
}
export default App;

思考问题:

  • 大家会觉得在App.js中componentDidMount输出的谁呢?

    • 直接看结果,发现输出的高阶组件所包裹的LogoProps 实例
  • 能不能会去到FancyInput.js Input ref呢?

    • 肯定是可以的,上述示例肯定不是我们想要的,继续往下看
// MyHoc.js
import React from 'react';

const MyHoc = (WrapperComponent) => {
   class LogoProps extends React.Component {
    render() {
      const { forwardRef, ...rest } = this.props;
      return (
        <WrapperComponent {...rest} ref={forwardRef} />
      )
    }
  }
  return React.forwardRef((props, ref) => (
    <LogoProps {...props} forwardRef={ref} />
  ))
}

export default MyHoc;
// FancyInput.js
import MyHoc from './MyHoc';
import React from 'react';

const FancyInput = React.forwardRef((props, ref) => {
  return (
    <input ref={ref}/>
  )
})

export default MyHoc(FancyInput)
//App.js
import React from 'react';
import './App.css';
import FancyInput from './RefsOfHOC/FancyInput';

class App extends React.Component {
  constructor(props) {
    super(props);
    this.forWardRef = React.createRef()
  }
  componentDidMount() {
    console.log(this.forWardRef)
  }
  render() {
    return (
      <div className="App">
        <FancyInput ref={this.forWardRef} />
      </div>
    )
  }
}
export default App;

我们来看看结果:

  • 拿到input Ref,就可以随意操作input组件了,例如:添加class,获取value值...等
  • 在上面的示例中,FancyInput 使用 React.forwardRef 来获取传递给它的 ref,然后转发到它渲染的 DOM input

以上内容如有遗漏错误,欢迎留言 ✍️指出,一起进步💪💪💪

如果觉得本文对你有帮助,🏀🏀留下你宝贵的 👍