React 高阶组件HOC用法归纳


Posted in Javascript onJune 13, 2021

一句话介绍HOC

何为高阶组件(HOC),根据官方文档的解释:“高阶组件是react中复用组件逻辑的一项高级技术。它不属于react API的组成部分,它是从react自身组合性质中抽离出来的一种模式。具体来说,高阶组件是函数,它接受一个组件作为参数,然后返回一个新的组件

使用场景

将几个功能相似的组件里面的方法和react特性(如生命周期里面的副作用)提取到HOC中,然后向HOC传入需要封装的组件。最后将公用的方法传给组件。

优势

使代码简洁优雅、代码量更少

HOC(高阶组件)

/*
  HOC(高阶组件): 接收一个组件,返回包装后的组件(增强组件)
    - 不是React API
    - 是一种设计模式,类似于装饰器模式
    - ≈ Mixin && > Minxin

  const 包装后的组件 = 高阶组件(被包装的组件);
  // e.g. const Wrapper = withRouter(NavBar);


  高阶组件会把所有接收到的props,传递给被包装的组件(透传)
  ref 和 key 类似,不是一个prop,所以不会透传,ref会绑定到外层的包装容器上 | 解决方法可以参考下面的 <<处理ref>>
* */

怎样包装组件?

/*
  怎样包装组件?

  第一种: 普通包装
    export时就包装
      import React from 'react';
      import Hoc from './Hoc';

      class Header extends React.Component {
        render() {
          return <span>{ this.props.count }</span>
        }
      };

      export default Hoc(Header);

    ==========

    import后再包装:
      import Header from './header';
      import Hoc from './Hoc';

      const EnhanceHeader = Hoc(Header);

      const Home = () => {
        return (
          <div>
             <EnhanceHeader count={1} />
          </div>
        )
      }

  第二种: 装饰器包装,只能在类组件中使用
    import React from 'react';
    import Hoc from './Hoc';

    @Hoc
    export default class Header extends React.Component {
      render() {
        return <span>{ this.props.count }</span>
      }
    };

    =======

    @Hoc
    class Header extends React.Component {
      render() {
        return <span>{ this.props.count }</span>
      }
    };

    export default Header;
* */

定义一个简单的HOC

/*
  定义一个简单的HOC,接收一个组件,返回一个组件

  import React from 'react';

  // 返回类组件
  export default function Hoc(WrappedComponent) {
    /*
      return class extends React.Component {}
        - 在 React Developer Tools 中展示的名字是 Component

      return class Wrapper extends React.Component {}
        - 在 React Developer Tools 中展示的名字是 Wrapper
    *\
    return class extends React.Component {
      render() {
        return <WrappedComponent {...this.props} />;
      }
    };
  }

  // 返回函数式组件
  export default function Hoc(WrappedComponent) {
    /*
      return function(props) {}
        - 在 React Developer Tools 中展示的名字是 Anonymous

      return function Wrapper(props) {}
        - 在 React Developer Tools 中展示的名字是 Wrapper
    *\
    return function Wrapper(props) {
      return <WrappedComponent {...props} />;
    };
  }
* */

给Hoc传参

/*
  给Hoc传参

   // Hoc,可以接受任意参数
    export default function Hoc(WrappedComponent, title, user, data) {
      return class Wrapper extends React.Component {
        render() {
          return <WrappedComponent {...this.props} />
        }
      };
    };

    // 包装时传参
    const EnhanceHeader = Hoc(Header, 'title', { name: '霖'}, [1, 2, 3]);
* */

Hoc嵌套

/*
  Hoc嵌套,函数柯里化的原理

  // Hoc1: 给组件添加title属性
  export default function Hoc1(WrappedComponent, title) {
    return class extends React.Component {
      render() {
        return <WrappedComponent title={title} {...this.props} />
      }
    };
  };

  // Hoc2: 修改组件的显示内容
  export default function Hoc2(WrappedComponent, content) {
    return class extends WrappedComponent { // 这里用了反向继承
      render() {
        const elementTree = super.render(); // React用Js对象来模拟Dom树结构,可以通过修改Js对象的属性来操纵数据

        console.log(elementTree); // 不太了解里面的结构可以打印出来 + 官网cloneElement() 了解一下

        const newElementTree = React.cloneElement(elementTree, { children: `你的内容已被劫持: ${content}` });

        return newElementTree;
      }
    };
  };

  // 被包裹的组件
  export default class Header extends React.Component {
    render() {
      const { title } = this.props;

      return (
          <span title={title}>
            默认内容
          </span>
        )
    }
  };

  // 使用
  import Hoc1 from './Hoc1';
  import Hoc2 from './Hoc2';

  /*
    包装过程
    1. const Wrapper = Hoc2(Header, '内容');
    2. Hoc1(Wrapper)
  **
  const EnhanceHeader = Hoc1(Hoc2(Header, '内容'), '标题');

  export default function Home() {
    return (
      <div>
        <EnhanceHeader />
      </div>
    );
  };

* */

处理ref

/*
  处理ref
  e.g. Hoc1(Hoc2(Content))

  <Content ref={myRef} /> 给Content绑定的ref会绑定到Hoc1上,且不会继续向下传递

  第一种方法 React.forwardRef ===============

      在 Hoc1外面 用React.forwardRef()对ref做处理,用props来传递ref
      0. 在高阶组件外面包裹forwardRef,拦截获取ref,增加一个props(xxx={ref}),真实组件通过props.xxx获取
      1. 使用时传 ref={XXXX}  // 和第二种方法不同的地方
      2. 用forwardRef的第二个参数获取 ref
      3. 增加一个新的props,用来向下转发ref  e.g. forwardedRef={ref}
      4. 真实组件中绑定 ref={props.forwardedRef}

      const Home = (props) => {
        const connectRef = useRef(null);

        return (
          <div>
            <Content ref={connectRef} />
          </div>
        );
      };

      // 被包装组件
      const Content = (props) => {
        return (
          <div>
            <input type="password" ref={props.forwardedRef} />
          </div>
        );
      };


      // forwardRef的第二个入参可以接收ref,在Hoc外层对ref做处理
      export default React.forwardRef((props, ref) => {
        const Wrapper = React.memo(Content);  // Hoc

        // forwardRef包裹的是Wrapper
        // 需要在Wrapper中把ref向下传递给真实组件
        // Wrapper中增加一个props属性,把ref对象作为props传给子组件
        return <Wrapper {...props} forwardedRef={ref} />;
      });

  第二种方法 ==========

  0. 使用时就用一个props来保存ref
  1. 使用时传 xxx={ref}  // 和第一种方法的不同点
  2. 真实组件中绑定 ref={props.xxx}

  const Home = (props) => {
    const connectRef = useRef(null);

    return (
      <div>
        <Content forwardedRef={connectRef} />
      </div>
    );
  };

  // 定义高阶组件
  export const Hoc = (WrappedComponent) => {
    class Wrapper extends React.Component {
      render() {
        return <WrappedComponent {...props} />
      }
    }
  }

  // 被包装的组件
  const Content = (props) => {
    return (
      <div>
        <input type="password" ref={props.forwardedRef} />
      </div>
    );
  };

  // 包装过程
  export default Hoc(Content);

* */

使用被包装组件的静态方法

/*
  使用被包装组件的静态方法

  // 被包装组件,增加静态属性和方法
  export default class Header extends React.Component {
    static displayName = 'header';
    static showName = () => {
      console.log(this.displayName);
    };

    render() {
      return <span>header</span>
    }
  };

  // HOC
  export default function Hoc(WrappedComponent) {
    return class Wrapper extends React.Component {
      render() {
        return <WrappedComponent {...this.props} />
      }
    };
  };

  ===========

  // Hoc包装后的组件拿不到静态方法
    import Header from './header';
    import Hoc from './Hoc';

    const EnhanceHeader = Hoc(Header);

    export default function Home() {
      console.log(EnhanceHeader.displayName);   // undefined
      EnhanceHeader.showName();                 // undefined

      return <EnhanceHeader />
    }

  =============

  // 解决方法1:拷贝静态方法到HOC上
    export default function Hoc(WrappedComponent) {
      return class Wrapper extends React.Component {
        static displayName = WrappedComponent.displayName;  // 必须知道被包装组件中有什么静态方法
        static showName = WrappedComponent.showName;

        render() {
          return <WrappedComponent {...this.props} />
        }
      };
    };

  ==============

  // 解决方法2:自动拷贝所有静态属性和方法
    import React from 'react';
    import hoistNonReactStatic from 'hoist-non-react-statics';

    export default function Hoc(WrappedComponent) {

      class Wrapper extends React.Component {
        render() {
          return <WrappedComponent {...this.props} />
        }
      };

      hoistNonReactStatic(Wrapper, WrappedComponent);
      return Wrapper;
    };

  ==============

    // 解决方法3:导出组件时,额外导入静态属性和方法
      class Header extends React.Component {
        render() {
          return <span>header</span>
        }
      };

      const displayName = 'header';

      function showName() {
        console.log(Header.displayName);
      };

      Header.displayName =displayName;
      Header.showName = showName;

      export default Header
      export { displayName, showName }

    // 导入时
      import Header, { displayName, showName } from './header';
      import Hoc from './Hoc';

      const EnhanceHeader = Hoc(Header);

      export default function Home() {
        console.log(displayName);   // header
        showName();                 // header

        return <EnhanceHeader />
      }
* */

拦截传给被包装组件的props,对props进行增删改

/*
  拦截传给被包装组件的props,对props进行增删改
  export default function Hoc(WrappedComponent) {

    return class Wrapper extends React.Component {
      render() {
        // 过滤一些仅在当前Hoc中使用的props,不进行不必要的透传
        const { forMeProps, forOtherProps } = this.props;

        // 在该HOC内部定义,需要注入到被包装组件的额外的属性或方法
        const injectProps = some-state-or-method;         // 通常是state或实例方法

        // 为被包装组件传递上层的props + 额外的props
        return (
          <WrappedComponent
            injectProps={injectProps}    // 传递需要注入的额外props
            {...forOtherProps}           // 透传与后续相关的props
          />
        )
      }
    }
  }

  e.g.
    Hoc接收一个额外的props 'dealUpper',如果为true,将data转换成大写
    dealUpper只在该Hoc中使用,所以没必要传给被包装的组件

  // HOC
  export default function Hoc(WrappedComponent) {
    return class Wrapper extends React.Component {
      render() {
        const { dealUpper, ...forOtherProps } = this.props;
        const { data } = forOtherProps;

        if (dealUpper) {
          Object.assign(forOtherProps, {data: data.toUpperCase()})
        }

        return <WrappedComponent {...forOtherProps} />
      }
    };
  };

  // 导出Hoc包装后的增强组件
  import React from 'react';
  import Hoc from './Hoc1';

  class Header extends React.Component {
    render() {
      console.log(this.props); // { data: 'ABC' }

      return <span>{this.props.data}</span>
    }
  };

  export default Hoc(Header); // 导出包装后的增强组件

  // 导入使用
  import Header from './header';

  const Home = () => {
    return <Header data={'abc'} dealUpper />
  }
* */

用HOC提取一些复杂的公共逻辑,在不同组件中扩展不同的功能

/*
  用HOC提取一些复杂的公共逻辑,在不同组件中扩展不同的功能
  import React from 'react';

  export const Hoc = (WrappedComponent, namespace) => {
    class Wrapper extends React.Component {
      state = {
        data: []
      }

      // 抽离的相同请求方法
      componentDidMount = () => {
        const { dispatch } = this.props;

        dispatch({
          type: `${namespace}/queryData`, // 动态请求不同的store
          payload: {},
          callback: res => {
            if (res) {
              this.setState({
                data: res.data
              })
            }
          }
        })
      }

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

  // 包装A组件
  import Hoc from './Hoc';

  const A = ({ data }) => {
    ... 省略请求数据的逻辑

    return (data.map(item => item));
  }

  export default MyHoc(A, 'a');

  // 包装B组件
  import Hoc from './Hoc';

  const B = ({ data }) => {
    ... 省略请求数据的逻辑

    return (
      <ul>
        {
          data.map((item, index) => {
            return <li key={index}><{item}/li>
          }
        }
      </ul>
    )
  }

   export default Hoc(B, 'b');
* */

让不受控组件变成受控组件

/*
  让不受控组件变成受控组件

  // Hoc组件
  export default function Hoc(WrappedComponent) {
    return class Wrapper extends React.Component {
      state = {
        value: ''
      };

      onChange = (e) => {
        this.setState({
          value: e.target.value
        })
      };

      render() {
        const newProps = {
          value: this.state.value,
          onChange: this.onChange
        };

        return <WrappedComponent {...this.props} {...newProps} />
      }
    };
  };

  // 普通组件
  class InputComponent extends React.Component {
    render() {
      return <input {...this.props} />
    }
  }

  // 包装
  export default Hoc(InputComponent);
* */

反向继承

/*
  反向继承(在Hoc中使用被包装组件内部的状态和方法)
    - 反向继承的组件要是类组件,函数组件不行

  export const Hoc = (WrappedComponent) => {
    class Wrapper extends WrappedComponent { // super ≈ WrappedComponent里面的this
      render() {
        if (!this.props.data) {
            return <span>loading....</span>
        } else {
            return super.render() // 调用被包装组件的render()方法
        }
      }
    }
  }

  ====

  export default function Hoc(WrappedComponent) {
    return class extends WrappedComponent {
      render() {
        const elementTree = super.render(); // React用Js对象来模拟Dom树结构,可以通过修改Js对象的属性来操纵数据

        console.log(elementTree); // 不太了解里面的结构可以打印出来 + 官网cloneElement() 了解一下

        const newElementTree = React.cloneElement(elementTree, { children: `你的内容已被劫持` });

        return newElementTree;
      }
    };
  };
* */

渲染劫持

/*
  渲染劫持

  e.g. 控制组件是否渲染(可以做全局的loading效果,没有数据时显示loading...)

    // 基本的实现
    export const LoadingHoc = (WrappedComponent) => {
      class Wrapper extends React.Component {
        render() {
          if (!this.props.data) {
            return <span>loading....</span>
          } else {
            return <WrappedComponent {...this.props} />
          }
        }
      }
    }

    // 用反向继承实现
    export const LoadingHoc = (WrappedComponent) => {
      class Wrapper extends WrappedComponent { // super ≈ WrappedComponent里面的this
        render() {
          if (!this.props.data) {
            return <span>loading....</span>
       	  } else {
            return super.render() // 调用被包装组件的render()方法
          }
        }
      }
    }

  ======

  e.g. 劫持渲染的内容

    export default function Hoc2(WrappedComponent) {
      return class extends WrappedComponent { // 这里用了反向继承
        render() {
          const elementTree = super.render(); // React用Js对象来模拟Dom树结构,可以通过修改Js对象的属性来操纵数据

          console.log(elementTree); // 不太了解里面的结构可以打印出来 + 官网cloneElement() 了解一下

          const newElementTree = React.cloneElement(elementTree, { children: `你的内容已被劫持` });

          return newElementTree;
        }
      };
    };
* */

配置包装名

/*
  配置包装名:在调试工具 React Developer Tools 中更容易被找到
  e.g. 高阶组件为Hoc,被包装组件为WrappedComponent, 显示的名字应该是 Hoc(WrappedComponent)


    // 返回类组件
    export default function Hoc(WrappedComponent) {
      return class extends React.Component {
        /*
          没有在Hoc中定义 static displayName = 'XXX';
            - React Developer Tools 中展示的名字是 Anonymous

          没有在被包装组件中定义 static displayName = 'XXX';
            - React Developer Tools 中展示的名字是 undefined Hoc

          在被包装组件中定义 static displayName = 'header';
            - React Developer Tools 中展示的名字是 header Hoc
        *\
        static displayName = `Hoc(${WrappedComponent.displayName});

        render() {
          return <WrappedComponent {...this.props} />;
        }
      };
    }

    // 返回函数式组件
    export default function Hoc(WrappedComponent) {

      /*
        return function(props) {}
          - 在 React Developer Tools 中展示的名字是 Anonymous

        return function Wrapper(props) {}
          - 在 React Developer Tools 中展示的名字是 Wrapper
      *
      return function Wrapper(props) {
        return <WrappedComponent {...props} />;
      };
    }

    =======

    export default function Hoc(WrappedComponent) {
      const Wrapper = (props) => {
        return <WrappedComponent {...props} />;
      };

      /*
        没有在被包装组件中定义 static displayName = 'XXX';
          - React Developer Tools 中展示的名字是 undefined Hoc

        在被包装组件中定义 static displayName = 'header';
          - React Developer Tools 中展示的名字是 header Hoc
      *\
      Wrapper.displayName = `Hoc(${WrappedComponent.displayName})`;

      return Wrapper;
    }

    =====


    // 被包裹组件
    export default class Header extends React.Component {
      static displayName = 'header';

      render() {
        return <span>{ this.props.count }</span>
      }
    };

* */

不要在render中使用HOC

/*
  不要在render中使用HOC

  e.g.
  export default class Home extends React.Component {
    render() {
      // 每次render都会创建一个新的Wrapper
      // Wrapper1 !== Wrapper2
      // 导致高阶组件会卸载和重新挂载,状态会丢失(e.g. checkbox的选中丢失 | state被清空)
      × const Wrapper = Hoc(WrappedComponent);

      return <Wrapper />
    }
  }

 =========

  √ const Wrapper = myHoc(WrappedComponent);

  export default class Home extends React.Component {
    render() {
      return <Wrapper />
    }
  }
* */

Hoc的渲染顺序

/*
  Hoc的渲染顺序
    Hoc(Header)

    componentDidMount: Header -> HOC
    componentWillUnMount: HOC -> Header
* */

HOC 和 Mixin

/*
  HOC 和 Mixin
    HOC
     - 属于函数式编程思想
     - 被包裹组件感知不到高阶组件的存在
     - 高阶组件返回的组件会在原来的基础上的到增强

    Mixin
    - 混入模式,会在被包装组件上不断增加新的属性和方法
    - 被包裹组件可感知
    - 需要做处理(命名冲突、状态维护)
* */

以上就是React 高阶组件HOC用法归纳的详细内容,更多关于React 高阶组件HOC的资料请关注三水点靠木其它相关文章!

Javascript 相关文章推荐
js宝典学习笔记(上)
Jan 10 Javascript
JS处理VBArray的函数使用说明
May 11 Javascript
JavaScript在IE和Firefox浏览器下的7个差异兼容写法小结
Jun 18 Javascript
中文路径导致unitpngfix.js不正常的解决方法
Jun 26 Javascript
js日期相关函数总结分享
Oct 15 Javascript
js表单处理中单选、多选、选择框值的获取及表单的序列化
Mar 08 Javascript
Bootstrap实现圆角、圆形头像和响应式图片
Dec 14 Javascript
使用jQuery实现鼠标点击左右按钮滑动切换
Aug 04 jQuery
解决layui 复选框等内置控件不显示的问题
Aug 14 Javascript
js+HTML5 canvas 实现简单的加载条(进度条)功能示例
Jul 16 Javascript
详解小程序如何改变onLoad的执行时机
Nov 01 Javascript
vue双向绑定数据限制长度的方法
Nov 04 Javascript
React forwardRef的使用方法及注意点
原生Javascript+HTML5一步步实现拖拽排序
JS代码编译器Monaco使用方法
React中的Context应用场景分析
Jun 11 #Javascript
详解JVM系列之内存模型
使用Vue3+Vant组件实现App搜索历史记录功能(示例代码)
一文搞懂redux在react中的初步用法
Jun 09 #Javascript
You might like
用PHP写的MySQL数据库用户认证系统代码
2007/03/22 PHP
PHP错误WARNING: SESSION_START() [FUNCTION.SESSION-START]解决方法
2014/05/04 PHP
php正则表达式验证(邮件地址、Url地址、电话号码、邮政编码)
2016/03/14 PHP
JS俄罗斯方块,包含完整的设计理念
2010/12/11 Javascript
js面向对象设计用{}好还是function(){}好(构造函数)
2011/10/23 Javascript
jQuery源码分析-04 选择器-Sizzle-工作原理分析
2011/11/14 Javascript
实例讲解jquery中mouseleave和mouseout的区别
2016/02/17 Javascript
微信小程序 css使用技巧总结
2017/01/09 Javascript
浅谈angular2的http请求返回结果的subcribe注意事项
2017/03/01 Javascript
轻松理解JavaScript之AJAX
2017/03/15 Javascript
详解vue.js之绑定class和style的示例代码
2017/08/24 Javascript
node使用Koa2搭建web项目的方法
2017/10/17 Javascript
写一个移动端惯性滑动&amp;回弹Vue导航栏组件 ly-tab
2018/03/06 Javascript
解决vue中使用swiper插件问题及swiper在vue中的用法
2018/04/04 Javascript
jQuery访问json文件中数据的方法示例
2019/01/28 jQuery
详解vue项目中使用token的身份验证的简单实践
2019/03/08 Javascript
JavaScript页面加载事件实例讲解
2019/09/01 Javascript
对layui中table组件工具栏的使用详解
2019/09/19 Javascript
Vue中关闭弹窗组件时销毁并隐藏操作
2020/09/01 Javascript
Openlayers测量距离与面积的实现方法
2020/09/25 Javascript
[01:07]2015国际邀请赛 中国区预选赛精彩回顾
2015/06/15 DOTA
[56:58]VP vs Optic 2018国际邀请赛小组赛BO2 第一场 8.16
2018/08/17 DOTA
用python生成1000个txt文件的方法
2018/10/25 Python
pytorch下大型数据集(大型图片)的导入方式
2020/01/08 Python
Pytorch 实现focal_loss 多类别和二分类示例
2020/01/14 Python
Python字符串格式化常用手段及注意事项
2020/06/17 Python
pyspark对Mysql数据库进行读写的实现
2020/12/30 Python
CSS3动画效果回调处理详解
2014/12/10 HTML / CSS
详解基于canvas的视频遮罩插件
2018/01/04 HTML / CSS
空字符串(“”)和null的区别
2012/11/13 面试题
阿里巴巴Oracle DBA笔试题答案-备份恢复类
2013/11/20 面试题
建筑专业自荐信范文
2014/01/05 职场文书
资助贫困学生倡议书
2014/05/16 职场文书
中国梦口号
2014/06/13 职场文书
食品安全处置方案
2014/06/14 职场文书
教师工作能力自我评价
2015/03/04 职场文书