写给本身的React HOC(高阶组件)手册

前言

HOC(高阶组件)是React中的一种组织代码的手段,而不是一个API. javascript

这种设计模式能够复用在React组件中的代码与逻辑,由于通常来说React组件比较容易复用渲染函数, 也就是主要负责HTML的输出.java

高阶组件其实是通过一个包装函数返回的组件,这类函数接收React组件处理传入的组件,而后返回一个新的组件.
注意:前提是创建在不修改原有组件的基础上.设计模式

文字描述太模糊,借助于官方文档稍稍修改,咱们能够更加轻松的理解高阶组件.数组

具体的实施

流程以下:性能优化

  1. 找出组件中复用的逻辑
  2. 建立适用于上方逻辑的函数
  3. 利用这个函数来建立一个组件
  4. enjoy it

找出组件中复用的逻辑

在实际开发中, 这种逻辑的组件很是常见:服务器

  1. 组件建立
  2. 向服务器拉取数据
  3. 利用数据渲染组件
  4. 监听数据的变化
  5. 数据变化或者触发修改的事件
  6. 利用变化后的数据再次渲染
  7. 组件销毁移除监听的数据源

首先咱们来建立一个生产假数据的对象来模拟数据源:app

const fakeDataGenerator = ()=>({
  timer: undefined,
  getData(){
    return ['hello', 'world'];
  },
  addChangeListener(handleChangeFun){ // 监听数据产生钩子

    if(this.timer){
      return;
    }

    this.timer = setInterval(()=> {
      handleChangeFun();
    },2000)
  },
  removeChangeListener(){ // 中止数据监听
    clearInterval(this.timer);
  }
});

而后来编写咱们的组件A:函数

const FakeDataForA = fakeDataGenerator();

class A extends React.Component {

  constructor(props) {// 1 组件建立
    super(props);

    this.state = {
      someData: fakeData.getData() // 1.1 向服务器拉取数据
    }

  }

  handleFakeDataChange = ()=>{ 
    this.setState({
      someData:fakeData.getData() // 4. 数据变化或者触发修改的事件
    });
  }

  componentDidMount(){
    // 3. 监听数据的变化
    // 4. 数据变化或者触发修改的事件
    fakeData.addChangeListener(this.handleFakeDataChange); 
  }

  componentWillUnmount(){
    fakeData.removeChangeListener(); // 6. 组件销毁移除监听的数据源
  }

  render() {
    return (
      {/*
        2. 利用数据渲染组件
        5. 利用变化后的数据再次渲染
      */}
      this.state.someData.map(name => (<span key={name}>{name}</span>)) 
    )
  }
}

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

而后咱们再来建立一个组件B这个虽然渲染方式不一样,可是数据获取的逻辑是一致的.
在通常的开发过程当中实际上也是遵循这个请求模式的,而后建立一个组件B:性能

const FakeDataForB = fakeDataGenerator();

class B extends React.Component {

  constructor(props) {// 1 组件建立
    super(props);

    this.state = {
      someData: fakeData.getData() // 1.1 向服务器拉取数据
    }

  }

  handleFakeDataChange = ()=>{ 
    this.setState({
      someData:fakeData.getData() // 4. 数据变化或者触发修改的事件
    });
  }

  componentDidMount(){
    // 3. 监听数据的变化
    // 4. 数据变化或者触发修改的事件
    fakeData.addChangeListener(this.handleFakeDataChange); 
  }

  componentWillUnmount(){
    fakeData.removeChangeListener(); // 6. 组件销毁移除监听的数据源
  }

  render() {
    return (
      {/*
        2. 利用数据渲染组件
        5. 利用变化后的数据再次渲染
      */}
      this.state.someData.map(name => (<div key={name}>{name}</div>)) 
    )
  }
}

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

这里我把redner中原来渲染的span标签改成了div标签,虽然这是一个小小的变化可是请你脑补这是两个渲染结果彻底不一样的组件好了. 优化

这时候问题以及十分明显了组件A和B明显有大量的重复逻辑可是借助于React组件却没法将这公用的逻辑来抽离.

在通常的开发中没有这么完美重复的逻辑代码,例如在生命周期函数中B组件可能多了几个操做或者A组件数据源获取的地址不一样.
可是这里依然存在大量的能够被复用的逻辑.

一个返回组件的函数

这种函数的第一个参数接收一个React组件,而后返回这个组件:

function MyHoc(Wrap) {
  return class extends React.Component{
    render(){
      <Wrap ></Wrap>
    }
  }
}

就目前来讲这个函数没有任何实际功能只是将原有的组件包装返回而已.

可是若是咱们将组件A和B传入到这个函数中,而使用返回的函数,咱们能够获得了什么.
咱们获取了在原有的组件上的一层包装,利用这层包装咱们能够把组件A和B的共同逻辑提取到这层包装上.

咱们来删除组件A和B有关数据获取以及修改的操做:

class A extends React.Component {

  componentDidMount(){
    // 这里执行某些操做 假设和另一个组件不一样
  }

  componentWillUnmount(){
    // 这里执行某些操做 假设和另一个组件不一样
  }

  render() {
    return (
      this.state.data.map(name => (<span key={name}>{name}</span>))
    )
  }
}

class B extends React.Component {

  componentDidMount(){
    // 这里执行某些操做 假设和另一个组件不一样
  }

  componentWillUnmount(){
    // 这里执行某些操做 假设和另一个组件不一样
  }

  render() {
    return (
      this.state.data.map(name => (<div key={name}>{name}</div>))
    )
  }
}

而后将在这层包装上的获取到的外部数据使用props来传递到原有的组件中:

function MyHoc(Wrap) {
  return class extends React.Component{

    constructor(props){

      super(props);

      this.state = {
        data:fakeData // 假设这样就获取到了数据, 先不考虑其余状况
      }

    }

    render(){
      return <Wrap data={this.state.data} {...this.props}></Wrap> {/* 经过 props 把获取到的数据传入 */}
    }
  }
}

在这里咱们在 HOC 返回的组件中获取数据, 而后把数据传入到内部的组件中, 那么数据获取的这种功能就被单独的拿了出来.
这样组件A和B只要关注本身的 props.data 就能够了彻底不须要考虑数据获取和自身的状态修改.

可是咱们注意到了组件A和B原有获取数据源不一样,咱们如何在包装函数中处理?

这点好解决,利用函数的参数差别来抹消掉返回的高阶组件的差别.

既然A组件和B组件的数据源不一样那么这个函数就另外接收一个数据源做为参数好了.
而且咱们将以前通用的逻辑放到了这个内部的组件上:

function MyHoc(Wrap,fakeData) { // 此次咱们接收一个数据源
  return class extends React.Component{

    constructor(props){

      super(props);
      this.state = {
        data: fakeData.getData() // 模拟数据获取
      }
      
    }

    handleDataChange = ()=>{
      this.setState({
        data:fakeData.getData()
      });
    }

    componentDidMount() {
      fakeData.addChangeListener(this.handleDataChange);
    }

    componentWillUnmount(){
      fakeData.removeChangeListener();
    }

    render(){
      <Wrap data={this.state.data} {...this.props}></Wrap>
    }
  }
}

利用高阶组件来建立组件

通过上面的思考,实际上已经完成了99%的工做了,接下来就是完成剩下的1%,把它们组合起来.

伪代码:

const
  FakeDataForA = FakeDataForAGenerator(),
  FakeDataForB = FakeDataForAGenerator(); // 两个不一样的数据源

function(Wrap,fakdData){ // 一个 HOC 函数
  return class extends React.Components{};
}

class A {}; // 两个不一样的组件
class B {}; // 两个不一样的组件

const 
  AFromHoc = MyHoc(A,FakeDataForA),
  BFromHoc = MyHoc(B,FakeDataForB); // 分别把不一样的数据源传入, 模拟者两个组件须要不一样的数据源, 可是获取数据逻辑一致

这个时候你就能够渲染本身的高阶组件AFromHocBFromHoc了.
这两个组件使用不一样的数据源来获取数据,通用的部分已经被抽离.

函数约定

HOC函数不要将多余的props传递给被包裹的组件

HOC函数须要像透明的同样,通过他的包装产生的新的组件和传入前没有什么区别.
这样作的目的在于,咱们不须要考虑通过HOC函数后的组件会产生什么变化而带来额外的心智负担.
若是你的HOC函数对传入的组件进行了修改,那么套用这种HOC函数屡次后返回的组件在使用的时候.
你不得不考虑这个组件带来的一些非预期行为.

因此请不要将本来组件不须要的props传入:

render() {
  // 过滤掉非此 HOC 额外的 props,且不要进行透传
  const { extraProp, ...passThroughProps } = this.props;

  // 将 props 注入到被包装的组件中。
  // 一般为 state 的值或者实例方法。
  const injectedProp = someStateOrInstanceMethod;

  // 将 props 传递给被包装组件
  return (
    <WrappedComponent
      injectedProp={injectedProp}
      {...passThroughProps}
    />
  );
}

HOC是函数!利用函数来最大化组合性

由于HOC是一个返回组件的函数,只要是函数能够作的事情HOC一样能够作到.
利用这一点,咱们能够借用在使用React以前咱们就已经学会的一些东西.

例如定义一个高阶函数用于返回一个高阶组件:

function HighLevelHoc(content) {
  return function (Wrap, className) {
    return class extends React.Component {
      render() {
        return (
          <Wrap {...this.props} className={className} >{content}</Wrap>
        )
      }
    }
  }
}

class Test extends React.Component {
  render() {
    return (
      <p>{this.props.children || 'hello world'}</p>
    )
  }
}

const H1Test = HighLevelHoc('foobar')(Test, 1);


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

或者干脆是一个不接收任何参数的函数:

function DemoHoc(Wrap) { // 用于向 Wrap 传入一个固定的字符串
  return class extends React.Component{
    render(){
      return (
        <Wrap {...this.props}>{'hello world'}</Wrap>
      )
    }
  } 
}

function Demo(props) {
  return (
    <div>{props.children}</div>
  )
}

const App = DemoHoc(Demo);

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

注意

不要在 render 方法中使用 HOC

咱们都知道 React 会调用 render 方法来渲染组件, 固然 React 也会作一些额外的工做例如性能优化.
在组件从新渲染的时候 React 会判断当前 render 返回的组件和未以前的组件是否相等 === 若是相等 React 会递归更新组件, 反之他会完全的卸载以前的旧的版原本渲染当前的组件.

HOC每次返回的内容都是一个新的内容:

function Hoc(){
  return {}
}
console.log( Hoc()===Hoc() ) // false

若是在 render 方法中使用:

render() {
  const DemoHoc = Hoc(MyComponent); // 每次调用 render 都会返回一个新的对象
  // 这将致使子树每次渲染都会进行卸载,和从新挂载的操做!
  return <DemoHoc />;
}

记得复制静态方法

React 的组件通常是继承 React.Component 的子类.
不要忘记了一个类上除了实例方法外还有静态方法, 使用 HOC 咱们对组件进行了一层包装会覆盖掉原来的静态方法:

class Demo extends React.Component{
  render(){
    return (
      <div>{this.props.children}</div>
    )
  }
}

Demo.echo = function () {
  console.log('hello world');
}

Demo.echo();// 是能够调用的

// -------- 定一个类提供一个静态方法

function DemoHoc(Wrap) {
  return class extends React.Component{
    render(){
      return (
        <Wrap>{'hello world'}</Wrap>
      )
    }
  } 
}

const App = DemoHoc(Demo);

// ----- HOC包装这个类

App.echo(); // error 这个静态方法不见了

解决方式

在 HOC 内部直接将原来组件的静态方法复制就能够了:

function DemoHoc(Wrap) {

  const myClass = class extends React.Component{
    render(){
      return (
        <Wrap>{'hello world'}</Wrap>
      )
    }
  }

  myClass.echo = Wrap.echo;

  return myClass;
}

不过这样一来 HOC 中就须要知道被复制的静态方法名是什么, 结合以前提到的灵活使用 HOC 咱们可让 HOC 接收静态方法参数名称:

function DemoHoc(Wrap,staticMethods=[]) { // 默认空数组

  const myClass = class extends React.Component{
    render(){
      return (
        <Wrap>{'hello world'}</Wrap>
      )
    }
  }

  for (const methodName of staticMethods) { // 循环复制
    myClass[methodName] = Wrap[methodName];
  }

  return myClass;
}

// -----
const App = DemoHoc(Demo,['echo']);

此外通常咱们编写组件的时候都是一个文件对应一个组件, 这时候咱们能够把静态方法导出.
HOC 不拷贝静态方法, 而是须要这些静态方法的组件直接引入就行了:

来自官方文档
// 使用这种方式代替...
MyComponent.someFunction = someFunction;
export default MyComponent;

// ...单独导出该方法...
export { someFunction };

// ...并在要使用的组件中,import 它们
import MyComponent, { someFunction } from './MyComponent.js';

透传 ref

ref 做为组件上的特殊属性, 没法像普通的 props 那样被向下传递.

例如咱们有一个组件, 咱们想使用 ref 来引用这个组件而且试图调用它的 echo 方法:

class Wraped extends React.Component{
  constructor(props){
    super(props);
    this.state = {
      message:''
    }
  }

  echo(){
    this.setState({
      message:'hello world'
    });
  }

  render(){
    return <div>{this.state.message}</div>
  }
}

咱们使用一个 HOC 包裹它:

function ExampleHoc(Wrap) {
  return class extends React.Component{
    render(){
      return <Wrap></Wrap>
    }
  }
}

const Example = ExampleHoc(Wraped);
// 获得了一个高阶组件

如今咱们把这个组件放入到 APP 组件中进行渲染, 而且使用 ref 来引用这个返回的组件, 而且试图调用它的 echo 方法:

const ref = React.createRef();

class App extends React.Component {

  handleEcho = () => {
    ref.current.echo();
  }

  render() {
    return (
      <div>
        <Example ref={ref}></Example>
        <button onClick={this.handleEcho}>echo</button> {/* 点击按钮至关于执行echo */}
      </div>
    )
  }
}

可是当你点击按钮试图触发子组件的事件的时候它不会起做用, 系统报错没有 echo 方法.

实际上 ref 被绑定到了 HOC 返回的那个匿名类上, 想要绑定到内部的组件中咱们能够进行 ref 透传.
默认的状况下 ref 是没法被进行向下传递的由于 ref 是特殊的属性就和 key 同样不会被添加到 props 中, 所以 React 提供了一个 API 来实现透传 ref 的这种需求.

这个 API 就是 React.forwardRef.

这个方法接收一个函数返回一个组件, 在这个含中它能够读取到组件传入的 ref , 某种意义上 React.forwardRef 也至关于一个高阶组件:

const ReturnedCompoent = React.forwardRef((props, ref) => {
  // 咱们能够获取到在props中没法获取的 ref 属性了
  return // 返回这个须要使用 ref 属性的组件
});

咱们把这个 API 用在以前的 HOC 中:

function ExampleHoc(Wrap) {
  class Inner extends React.Component {
    render() {
      const { forwardedRef,...rest} = this.props;
      return <Wrap ref={forwardedRef} {...rest} ></Wrap> // 2. 咱们接收到 props 中被更名的 ref 而后绑定到 ref 上
    }
  }
  return React.forwardRef((props,ref)=>{ // 1. 咱们接收到 ref 而后给他更名成 forwardedRef 传入到props中
    return <Inner {...props} forwardedRef={ref} ></Inner>
  })
}

这个时候在调用 echo 就没有问题了:

handleEcho = () => {
  ref.current.echo();
}
相关文章
相关标签/搜索