聊聊React高阶组件(Higher-Order Components)

使用 react已经有不短的时间了,最近看到关于 react高阶组件的一篇文章,看了之后顿时眼前一亮,对于我这种还在新手村晃荡、一切朝着打怪升级看齐的小喽啰来说,像这种难度不是太高同时门槛也不是那么低的东西如今可不多见了啊,是个不可多得的 zhuangbility的利器,自然不可轻易错过,遂深入了解了一番。


概述

高阶组件的定义

React 官网上对高阶组件的定义:

高阶部件是一种用于复用组件逻辑的高级技术,它并不是 React API的一部分,而是从React 演化而来的一种模式。 具体地说,高阶组件就是一个接收一个组件并返回另外一个新组件的函数。 相比于普通组件将 props 转化成界面UI,高阶组件将一个普通组件转化为另外一个组件。

大概意思就是说, HOC并不是 reactAPI的一部分,而是一种实现的模式,有点类似于 观察者模式单例模式之类的东西,本质还是函数。


功能

既然是能够拿来 zhuangbility的利器,那么不管怎么说,简单实用的招式必不可少,可以利用高阶组件来做的事情:

  1. 代码复用,逻辑抽象,抽离底层准备(bootstrap)代码
  2. Props 更改
  3. State 抽象和更改
  4. 渲染劫持

用法示例

基本用法

  • 一个最简单的高阶组件(HOC) 示例如下:

     1 // HOCComponent.js
     2
     3 import React from ‘react‘
     4
     5 export default PackagedComponent =>
     6   class HOC extends React.Component {
     7     render() {
     8       return (
     9         <div id="HOCWrapper">
    10           <header>
    11             <h1>Title</h1>
    12           </header>
    13           <PackagedComponent/>
    14         </div>
    15       )
    16     }
    17   }

  

此文件导出了一个函数,此函数返回经过一个经过处理的组件,它接受一个参数 PackagedComponent,此参数就是将要被 HOC包装的普通组件,接受一个普通组件,返回另外一个新的组件,很符合高阶组件的定义。

  • 此高阶组件的简单使用如下:
 1 // main.js
 2 import React from ‘react‘
 3 // (1)
 4 import HOCComponent from ‘./HOCComponent‘
 5
 6 // (2)
 7 @HOCComponent
 8 class Main extends React.Component {
 9   render() {
10     return(
11       <main>
12         <p>main content</p>
13       </main>
14     )
15   }
16 }
17 // (2)
18 // 也可以将上面的 @HOCComponent换成下面这句
19 //  const MainComponent = HOCComponent(Main)
20 export default MainComponent

想要使用高阶组件,首先(1)将高阶组件导入,然后(2)使用此组件包装需要被包装的普通组件 Main,这里的@符号是 ES7中的decorator,写过Java或者其他静态语言的同学应该并不陌生,这实际上就是一个语法糖,可以使用 react-decorators 进行转换, 在这里相当于下面这句代码:

const MainComponent = HOCComponent(Main)

@HOCComponent完全可以换成上面那句,只不过需要注意的是,类不具有提升的能力,所以若是觉得上面那句顺眼换一下,那么在换过之后,还要将这一句的位置移到类Main定义的后面。

最后,导出的是被高阶组件处理过的组件 MainComponent

  • 这样,就完成了一个普通组件的包装,可以在页面上将被包装过的组件显示出来了:
 1 import React from ‘react‘
 2 import { render } from ‘react-dom‘
 3
 4 // 导入组件
 5 import MainComponent from ‘./main‘
 6
 7 render(
 8   <MainComponent/>,
 9   document.getElementById(‘root‘)
10 )

页面显示如下:

可以使用 React Developer Tools查看页面结构:

可以看出,组件Main的外面包装了一层 HOC,有点类似于父组件和子组件,但很显然高阶组件并不等于父组件。

另外需要注意的一点, HOC这个高阶组件,我们可能会用到不止一次,功能技术上没什么关系,但是不利于调试,为了快速地区分出某个普通组件的所属的HOC到底是哪一个,我们可以给这些 HOC进行命名:

 1 // 获取传入的被包装的组件名称,以便为 HOC 进行命名
 2 let getDisplayName = component => {
 3   return component.displayName || component.name || ‘Component‘
 4 }
 5
 6 export default PackagedComponent =>
 7   class HOC extends React.Component {
 8     // 这里的 displayName就指的是 HOC的显示名称,我们将它重新定义了一遍
 9     // static被 stage-0  stage-1 和 stage-2所支持
10     static displayName = `HOC(${getDisplayName(PackagedComponent)})`
11     render() {
12       return (
13         <div id="HOCWrapper">
14           <header>
15             <h1>Title</h1>
16           </header>
17           <PackagedComponent/>
18         </div>
19       )
20     }
21   }

现在的 DOM结构:

可以看到,原先的HOC已经变成了 HOC(Main)了,这么做主要是利于我们的调试开发。

这里的HOC,可以看做是一个简单的为普通组件增加Title的高阶组件,但是很明显并不是所有的页面都只使用同一个标题,标题必须要可定制化才符合实际情况。

想做到这一点也很简单,那就是再为HOC组件的高阶函数增加一个 title参数,另外考虑到 柯里化 Curry函数和函数式编程,我们修改后的 HOC代码如下:

 1 // HOCComponent.js
 2
 3 // 增加了一个函数,这个函数存在一个参数,此参数就是要传入的`title`
 4 export default PackagedComponent => componentTitle =>
 5   class HOC extends React.Component {
 6     static displayName = `HOC(${getDisplayName(PackagedComponent)})`
 7     render() {
 8       return (
 9         <div id="HOCWrapper">
10           <header>
11             <h1>{ componentTitle ? componentTitle : ‘Title‘ }</h1>
12           </header>
13           <PackagedComponent/>
14         </div>
15       )
16     }
17   }

使用方式如下:

1 // main.js
2
3 // ...省略代码
4 const MainComponent = HOCComponent(Main)(‘首页‘)
5 export default MainComponent

然后在页面上就可以看到效果了:


属性代理

HOC是包裹在普通组件外面的一层高阶函数,任何要传入普通组件内的props 或者 state 首先都要经过 HOC

props和 state等属性原本是要流向 目标组件的腰包的,但是却被 雁过拔毛的HOC拦路打劫,那么最终这些 props和 states数据到底还能不能再到达 目标组件,或者哪些能到达以及到达多少就全由 HOC说了算了,也就是说,HOC拥有了提前对这些属性进行修改的能力。

更改 Props

对 Props 的更改操作包括 增、删、改、查,在修改和删除 Props的时候需要注意,除非特殊要求,否则最好不要影响到原本传递给普通组件的 Props

 1 class HOC extends React.Component {
 2     static displayName = `HOC(${getDisplayName(PackagedComponent)})`
 3     render() {
 4       // 向普通组件增加了一个新的 `Props`
 5       const newProps = {
 6         summary: ‘这是内容‘
 7       }
 8       return (
 9         <div id="HOCWrapper">
10           <header>
11             <h1>{ componentTitle ? componentTitle : ‘Title‘ }</h1>
12           </header>
13           <PackagedComponent {...this.props} {...newProps}/>
14         </div>
15       )
16     }
17   }

通过 refs 获取组件实例

普通组件如果带有一个 ref属性,当其通过 HOC的处理后,已经无法通过类似 this.refs.component的形式获取到这个普通组件了,只会得到一个被处理之后的组件,想要仍然获得原先的普通组件,需要对 ref进行处理,一种处理方法类似于 react-readux 中的 connect方法,如下:

 1 // HOCComponnet.js
 2 ...
 3 export default PackagedComponent => componentTitle =>
 4   class HOC extends React.Component {
 5     static displayName = `HOC(${getDisplayName(PackagedComponent)})`
 6     // 回调方法,当被包装组件渲染完毕后,调用被包装组件的 changeColor 方法
 7     propc(wrapperComponentInstance) {
 8       wrapperComponentInstance.changeColor()
 9     }
10     render() {
11       // 改变 props,使用 ref 获取被包装组件的示例,以调用其中的方法
12       const props = Object.assign({}, this.props, {ref: this.propc.bind(this)})
13       return (
14         <div id="HOCWrapper">
15           <header>
16             <h1>{ componentTitle ? componentTitle : ‘Title‘ }</h1>
17           </header>
18           <PackagedComponent {...props}/>
19         </div>
20       )
21     }
22   }

使用:

 1 // main.js
 2 ...
 3 class Main extends React.Component {
 4   render() {
 5     return(
 6       <main>
 7         <p>main content</p>
 8         <span>{ this.props.summary }</span>
 9       </main>
10     )
11   }
12   // main.js 中的changeColor 方法
13   changeColor() {
14     console.log(666);
15     document.querySelector(‘p‘).style.color = ‘greenyellow‘
16   }
17 }
18 ...

反向继承(Inheritance Inversion)

相比于前面使用 HOC包装在 普通组件外面的情况,反向继承就是让HOC继承普通组件、打入普通组件的内部,这种更厉害,前面还只是拦路打劫,到了这里就变成暗中潜伏了,这种情况下,普通组件变成了基类,而HOC变成了子类,子类能够获得父类所有公开的方法和字段。

反向继承高阶组件的功能:

  1. 能够对普通组件生命周期内的所有钩子函数进行覆写
  2. 对普通组件的 state进行增删改查的操作。
 1 // HOCInheritance.js
 2
 3 let getDisplayName = (component)=> {
 4   return component.displayName || component.name || ‘Component‘
 5 }
 6
 7 // (1)
 8 export default WrapperComponent =>
 9 class Inheritance extends WrapperComponent {
10   static displayName = `Inheritance(${getDisplayName(WrapperComponent)})`
11   // (2)
12   componentWillMount() {
13     this.state.name = ‘zhangsan‘
14     this.state.age = 18
15   }
16   render() {
17     // (4)
18     return super.render()
19   }
20   componentDidMount() {
21     // 5
22     super.componentDidMount()
23     // 6
24     document.querySelector(‘h1‘).style.color = ‘indianred‘
25   }
26 }

上述代码中,让 Inheritance 继承 WrapperComponent (1)

并且覆写了WrapperComponent 中的 componentWillMount函数(2)

在这个方法中对 WrapperComponent 的 state进行操作(3)

在 render方法中,为了防止破坏WrapperComponent原有的 render()方法,使用 super将 WrapperComponent 中原有的 render方法实现了一次(4)

在 componentDidMount同样是先将 WrapperComponent 中的 componentDidMount方法实现了一次(5)

并且在原有的基础上,又进行了一些额外的操作(6)

super并不是必须使用,这取决于你是否需要实现普通组件中原有的对应函数,一般来说都是需要的,类似于 mixin,至于到底是原有钩子函数中的代码先执行,还是 HOC中另加的代码先执行,则取决于 super的位置,如果super在新增代码之上,则原有代码先执行,反之亦然。

另外,如果普通组件并没有显性实现某个钩子函数,然后在HOC中又添加了这个钩子函数,则 super不可用,因为并没有什么可以 super的,否则将报错。

使用:

 1 // main2.js
 2
 3 import React from ‘react‘
 4 import Inheritance from ‘./HOCInheritance‘
 5
 6 class Main2 extends React.Component {
 7   state = {
 8     name: ‘wanger‘
 9   }
10   render() {
11     return (
12       <main>
13         <h1>summary of </h1>
14         <p>
15           my name is {this.state.name},
16           I‘m {this.state.age}
17         </p>
18       </main>
19     )
20   }
21
22   componentDidMount() {
23     document.querySelector(‘h1‘).innerHTML += this.state.name
24   }
25 }
26
27 const InheritanceInstace = Inheritance(Main2)
28 export default InheritanceInstace

页面效果:

可以看出,HOC为原有组件添加了 componentWillMount函数,在其中覆盖了 Main2中 state的 ‘name‘属性,并且其上添加了一个age属性

HOC还将 Main的 componentDidMount方法实现了一次,并且在此基础上,实现了自己的 componentDidMount方法。


用法拓展

HOC的用处很多,例如代替简单的父组件传递props,修改组件的props数据等,除此之外,基于以上内容,我还想到了另外一种让 HOC配合 redux的使用技巧。

用过vue与 vuex的人都知道,这两个可谓是天作之合的一对好基友,后者基本上就是为前者量身定做,贴心的很,几乎不用多做什么事情,就能在 vue的任何组件中获取存储在 vuex中的数据,例如:

this.$store.state.data

只要 vuex中存储了 data这个值,那么一般情况下,在 vue的任何组件中,都是可以通过上面的一行代码获取到 data的。

至于,react和 redux,看起来似乎和 vuevuex之间的关系差不多,用起来似乎也是二者搭配干活不累,but,实际上他们之间的关系并没有那么铁。

redux能够搭配的东西不仅是react,还有 jqueryvueAngularEmber等任意框架,原生 js也 ok,颇有种搭天搭地搭空气的倾向,所以,其与react之间肯定不可能像 vuevuex那么融洽和谐。

因而,如果你想在react中像在 vue中那么毫不费力地通过类似于以下代码在任意 react组件中获取到 redux中的数据,那么我只能说,你大概又写了个 bug

this.$store.state.data

当然,如果你非要像这样获取到数据,也是可以的,但肯定要多费些手脚,一般在react中获取 redux中数据的方法都要像这样:

 1 // 首先,导入相关文件
 2 import { bindActionCreators } from ‘redux‘
 3 import { connect } from ‘react-redux‘
 4 import * as commonInfoActionsFromOtherFile from ‘actions/commoninfo.js‘
 5
 6 // ...
 7
 8 // 然后,传递数据和方法
 9
10 let mapStateToProps = (state)=>{
11   return {
12     commonInfo: state.commonInfo
13   }
14 }
15
16 let mapDispatchToProps = (dispatch)=>{
17   return {
18     commonInfoActions: bindActionCreators(commonInfoActionsFromOtherFile, dispatch)
19   }
20 }
21 // 最终,将组件导出
22 export default connect(
23   mapStateToProps,
24   mapDispatchToProps
25 )(ExampleComponent)

代码其实也不是太多,但如果每次想要在一个组件获取 redux中的数据和方法都要将这段代码写一遍,实在是有些啰嗦。

一种解决方法就是将 redux中所有的数据和 dispatch方法全都暴露给根组件,让根组件往下传递到所有的子组件中,这确实是一种方法,但似乎有些冗余了, redux中的数据暴露在项目所有组件中,但有些组件根本用不到 redux中的数据,干嘛还非要塞给它?

而另外一种方法,就是要用到本文所说的 HOC了。

既然高阶组件能够代理到 普通组件的Props 和 state等属性,那么在使用诸如 redux等库的时候,是不是可以让高阶组件来承接这些由 redux传递到全局的属性,然后再用高阶组件包装普通组件,将获得的属性传递给普通组件,这样普通组件就能获取到 这些全局属性了。

相比于使用 redux一个个地初始化所有需要使用到全局属性的组件,使用高阶组件作为载体,虽然结构上多了一层,但是操作上明显方便简化了许多。

理论上可行,但无图无代码,嘴上说说可没用,我特地实验了一番,已用实践证实了其可行性。

一种封装 HOC,让其承载 redux 的示例代码如下:

 1 // HocRedux.js
 2
 3 import { bindActionCreators } from ‘redux‘
 4 import { connect } from ‘react-redux‘
 5 import * as actionsLists from ‘../actions/actionsLists‘
 6
 7 let getDisplayName = component=> {
 8   return component.displayName || component.name || ‘Component‘
 9 }
10
11 let mapStateToProps = (state)=>{
12   return {
13     reduxState: state
14   }
15 }
16 let mapDispatchToProps = (dispatch)=>{
17   return {
18     reduxActions: bindActionCreators(actionsLists, dispatch)
19   }
20 }
21
22 export default ChildComponent =>
23 connect(
24   mapStateToProps,
25   mapDispatchToProps
26 )(class HocInheritance extends ChildComponent {
27   static displayName = `HocInheritance(${getDisplayName(ChildComponent)})`
28 })

然后,普通组件被此HOC处理后,就可以轻松获取 redux中的数据了,想让哪个组件获取 redux,哪个组件就能获取到,不想获取的就获取不到,简单明了,使用方法和上面一样:

1 import HocRedux from ‘HocRedux‘
2 // 省略代码
3 const InheritanceInstace = Inheritance(Main2)
4 export default InheritanceInstace

注意事项

react官网 上还给出了几条关于使用 HOC 时的注意事项。

  • 不要在render函数中使用高阶组件

例如,以下就是错误示范:

1 // 这是个 render 方法
2 render() {
3   // 在 render 方法中使用了 HOC
4   // 每一次render函数调用都会创建一个新的EnhancedComponent实例
5   // EnhancedComponent1 !== EnhancedComponent2
6   const EnhancedComponent = enhance(MyComponent);
7   // 每一次都会使子对象树完全被卸载或移除
8   return <EnhancedComponent />;
9 }
  • 静态方法必须复制

HOC 虽然可以自动获得 普通组件的 props和 state等属性,但静态方法必须要手动挂载。

1 // 定义静态方法
2 WrappedComponent.staticMethod = function() {/*...*/}
3 // 使用高阶组件
4 const EnhancedComponent = enhance(WrappedComponent);
5
6 // 增强型组件没有静态方法
7 typeof EnhancedComponent.staticMethod === ‘undefined‘ // true

为了解决这个问题,在返回之前,可以向容器组件中复制原有的静态方法:

1 function enhance(WrappedComponent) {
2   class Enhance extends React.Component {/*...*/}
3   // 必须得知道要拷贝的方法
4   Enhance.staticMethod = WrappedComponent.staticMethod;
5   return Enhance;
6 }

或者使用 hoist-non-react-statics来自动复制这些静态方法

  • Refs不会被传递 对于 react组件来说,ref其实不是一个属性,就像key一样,尽管向其他props一样传递到了组件中,但实际上在组件内时获取不到的,它是由React特殊处理的。如果你给高阶组件产生的组件的元素添加 ref,ref引用的是外层的容器组件的实例,而不是被包裹的组件。

想要解决这个问题,首先是尽量避免使用 ref,如果避免不了,那么可以参照本文上面提到过的方法。

如果你喜欢我们的文章,关注我们的公众号和我们互动吧。

时间: 2024-10-17 13:47:05

聊聊React高阶组件(Higher-Order Components)的相关文章

react高阶组件的理解

[高阶组件和函数式编程] function hello() { console.log('hello jason'); } function WrapperHello(fn) { return function() { console.log('before say hello'); fn(); console.log('after say hello'); } } // hello 这时候等于 WrapperHello函数中返回的 匿名函数 // 在设计模式中这种操作叫做 装饰器模式 // 高

react:高阶组件wrappedComponent

什么是高阶组件? 高阶部件是一种用于复用组件逻辑的高级技术,它并不是 React API的一部分,而是从React 演化而来的一种模式. 具体地说,高阶组件就是一个接收一个组件并返回另外一个新组件的函数! 解决什么问题? 随着项目越来越复杂,开发过程中,多个组件需要某个功能,而且这个功能和页面并没有关系,所以也不能简单的抽取成一个新的组件,但是如果让同样的逻辑在各个组件里各自实现,无疑会导致重复的代码.比如页面有三种弹窗一个有title,一个没有,一个又有右上角关闭按钮,除此之外别无它样,你总不

react高阶组件

高阶函数基本概念: 函数可以作为参数被传递, 函数可以作为返回值输出 高阶组件的基本概念: 高阶组件就是接受一个组件作为参数并返回一个新组件的函数 高级组件是一个函数,并不是组件 代理方式的高阶组件:返回的新组件类直接继承自React.Component类.新组件扮演的角色传入参数组件的一个代理,在新组件的render函数中,将被包裹组件渲染出来,除了高阶组件自己要做得工作,其余功能全都转手给了包裹的组件 代理方式的高阶组件用途: 1--操作prop         2--访问ref    3-

react 高阶组件之小学版

高阶组件  多么高大上的概念,一般用来实现组件逻辑的抽象和复用,在很多三方库(redux)中都被使用到,但是开发普通有任务项目时,如果能合理使用高阶组件,也会显著的提高代码质量. 我们今天就用最简单的逻辑来搞一搞这个家伙 我们先看一个栗子,看看这个家伙是如何进行逻辑复用的: 现在有一个组件MyComponent,需要从LocalStorage中获取数据, 然后渲染到界面. 一般情况下,我们可以这样实现: 代码很简单,但当其他组件也需要从LocalStorage中获取同样的数据展示出来时,每个组件

React: 高阶组件(HOC)

一.简介 如我们所知,JavaScript有高阶函数这么一个概念,高阶函数本身是一个函数,它会接收或者返回一个函数,进而对该函数进行操作.其实,在React中同样地有高阶组件这么一个东西,称为HOC,它也是一个函数,但是与高阶函数不同的是,高阶组件操作的是组件,它会接收一个组件作为参数,然后返回另外一个组件.通常,HOC会使用一个能够维护State或者包含若干功能的类来包装输入的组件,父组件保留State或者将若干功能作为属性向下传递给参数组件,参数组件不需要知道HOC代码实现的具体细节,它允许

高阶函数HOF和高阶组件HOC(Higher Order Func/Comp)

一.什么是高阶函数(组件),作用是什么? 子类使用父类的方法可以通过继承的方式实现,那无关联组件通信(redux).父类使用子类方法(反向继承)呢 为了解决类(函数)功能交叉/功能复用等问题,通过传入类/函数返回类/函数(继承)的方式使得类拥有自身未定义的方法. 例如react-redux的connect方法使用了高阶组件: React Redux的connect: const HOC = connnect(mapStateToProps)(Comp); // connect为柯里化函数 实际为

react.js 高阶组件----很简单的实例理解高阶组件思想

/* * 高阶组件其实是一个函数,传进去的一个组件,返回一个新组件 * 实现不同组件中的逻辑复用, * 将一些可以单独抽离的逻辑处理给要返回的新组件里面复用 * 然后将单独的组件,传递给新组件 * */ import React, {Component} from 'react' import ReactDOM from 'react-dom' //高阶组件定义,里面return 返回新组件 function local(Comp,key){ class Proxy extends Compon

React组件重构:嵌套+继承 与 高阶组件

前言 在最近做的一个react项目中,遇到了一个比较典型的需要重构的场景:提取两个组件中共同的部分. 最开始通过使用嵌套组件和继承的方式完成了这次重构. 但是后来又用高阶组件重新写了一遍,发现更好一点. 在这里记录下这两种方式以便之后参考和演进. 本次重构的场景 因为场景涉及到具体的业务,所以我现在将它简化为一个简单的场景. 现在有两个黑色箱子,箱子上都有一个红色按钮,A箱子充满气体,按了按钮之后箱子里面气体变红,B箱子充满泥土,按了之后箱子里面泥土变红. 那么现在上一个简单的重构前代码: Bo

【转】react的高阶组件

前言 本文代码浅显易懂,思想深入实用.此属于react进阶用法,如果你还不了解react,建议从文档开始看起. 我们都知道高阶函数是什么, 高阶组件其实是差不多的用法,只不过传入的参数变成了react组件,并返回一个新的组件. A higher-order component is a function that takes a component and returns a new component. 形如: const EnhancedComponent = higherOrderComp