高阶组件

前言

本文代码浅显易懂,思想深入实用。此属于react进阶用法,如果你还不了解react,建议从文档开始看起。

我们都知道高阶函数是什么, 高阶组件其实是差不多的用法,只不过传入的参数变成了react组件,并返回一个新的组件.

A higher-order component is a function that takes a component and
returns a new component.

形如:

const EnhancedComponent = higherOrderComponent(WrappedComponent);

高阶组件是react应用中很重要的一部分,最大的特点就是重用组件逻辑。它并不是由React API定义出来的功能,而是由React的组合特性衍生出来的一种设计模式。如果你用过redux,那你就一定接触过高阶组件,因为react-redux中的connect就是一个高阶组件。

原文https://github.com/sunyongjian/blog/issues/25

引入

先来一个最简单的高阶组件

import React, { Component } from ‘react‘;
import simpleHoc from ‘./simple-hoc‘;

class Usual extends Component {
  render() {
    console.log(this.props, ‘props‘);
    return (
      <div>
        Usual
      </div>
    )
  }
}
export default simpleHoc(Usual);
import React, { Component } from ‘react‘;

const simpleHoc = WrappedComponent => {
  console.log(‘simpleHoc‘);
  return class extends Component {
    render() {
      return <WrappedComponent {...this.props}/>
    }
  }
}
export default simpleHoc;

组件Usual通过simpleHoc的包装,打了一个log... 那么形如simpleHoc就是一个高阶组件了,通过接收一个组件class Usual,并返回一个组件class。 其实我们可以看到,在这个函数里,我们可以做很多操作。 而且return的组件同样有自己的生命周期,function,另外,我们看到也可以把props传给WrappedComponent(被包装的组件)。

装饰器模式

高阶组件可以看做是装饰器模式(Decorator Pattern)在React的实现。即允许向一个现有的对象添加新的功能,同时又不改变其结构,属于包装模式(Wrapper Pattern)的一种

ES7中添加了一个decorator的属性,使用@符表示,可以更精简的书写。那上面的例子就可以改成:

import React, { Component } from ‘react‘;
import simpleHoc from ‘./simple-hoc‘;

@simpleHoc
export default class Usual extends Component {
  render() {
    return (
      <div>
        Usual
      </div>
    )
  }
}

是同样的效果。
当然兼容性是存在问题的,通常都是通过babel去编译的。 babel提供了plugin,高阶组件用的是类装饰器,所以用transform-decorators-legacy babel

两种形式

属性代理

引入里我们写的最简单的形式,就是属性代理(Props Proxy)的形式。通过hoc包装wrappedComponent,也就是例子中的Usual,本来传给Usual的props,都在hoc中接受到了,也就是props proxy。 由此我们可以做一些操作

操作props

最直观的就是接受到props,我们可以做任何读取,编辑,删除的很多自定义操作。包括hoc中定义的自定义事件,都可以通过props再传下去。

import React, { Component } from ‘react‘;

const propsProxyHoc = WrappedComponent => class extends Component {

  handleClick() {
    console.log(‘click‘);
  }

  render() {
    return (<WrappedComponent
      {...this.props}
      handleClick={this.handleClick}
    />);
  }
};
export default propsProxyHoc;

然后我们的Usual组件render的时候, console.log(this.props) 会得到handleClick.

refs获取组件实例

当我们包装Usual的时候,想获取到它的实例怎么办,可以通过引用(ref),在Usual组件挂载的时候,会执行ref的回调函数,在hoc中取到组件的实例。通过打印,可以看到它的props, state,都是可以取到的。

import React, { Component } from ‘react‘;

const refHoc = WrappedComponent => class extends Component {

  componentDidMount() {
    console.log(this.instanceComponent, ‘instanceComponent‘);
  }

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

export default refHoc;

抽离state

这里不是通过ref获取state, 而是通过 { props, 回调函数 } 传递给wrappedComponent组件,通过回调函数获取state。这里用的比较多的就是react处理表单的时候。通常react在处理表单的时候,一般使用的是受控组件(文档),即把input都做成受控的,改变value的时候,用onChange事件同步到state中。当然这种操作通过Container组件也可以做到,具体的区别放到后面去比较。看一下代码就知道怎么回事了:

// 普通组件Login
import React, { Component } from ‘react‘;
import formCreate from ‘./form-create‘;

@formCreate
export default class Login extends Component {
  render() {
    return (
      <div>
        <div>
          <label id="username">
            账户
          </label>
          <input name="username" {...this.props.getField(‘username‘)}/>
        </div>
        <div>
          <label id="password">
            密码
          </label>
          <input name="password" {...this.props.getField(‘password‘)}/>
        </div>
        <div onClick={this.props.handleSubmit}>提交</div>
        <div>other content</div>
      </div>
    )
  }
}
//HOC
import React, { Component } from ‘react‘;

const formCreate = WrappedComponent => class extends Component {

  constructor() {
    super();
    this.state = {
      fields: {},
    }
  }
  onChange = key => e => {
    const { fields } = this.state;
    fields[key] = e.target.value;
    this.setState({
      fields,
    })
  }
  handleSubmit = () => {
    console.log(this.state.fields);
  }
  getField = fieldName => {
    return {
      onChange: this.onChange(fieldName),
    }
  }
  render() {
    const props = {
      ...this.props,
      handleSubmit: this.handleSubmit,
      getField: this.getField,
    }
    return (<WrappedComponent
      {...props}
    />);
  }
};
export default formCreate;

这里我们把state,onChange等方法都放到HOC里,其实是遵从的react组件的一种规范,子组件简单,傻瓜,负责展示,逻辑与操作放到Container。比如说我们在HOC获取到用户名密码之后,再去做其他操作,就方便多了,而state,处理函数放到Form组件里,只会让Form更加笨重,承担了本不属于它的工作,这样我们可能其他地方也需要用到这个组件,但是处理方式稍微不同,就很麻烦了。

反向继承

反向继承(Inheritance Inversion),简称II,本来我是叫继承反转的...因为有个模式叫控制反转嘛...
跟属性代理的方式不同的是,II采用通过 去继承WrappedComponent,本来是一种嵌套的关系,结果II返回的组件却继承了WrappedComponent,这看起来是一种反转的关系。
通过继承WrappedComponent,除了一些静态方法,包括生命周期,state,各种function,我们都可以得到。上栗子:

 // usual
import React, { Component } from ‘react‘;
import iiHoc from ‘./ii-hoc‘;

@iiHoc
export default class Usual extends Component {

  constructor() {
    super();
    this.state = {
      usual: ‘usual‘,
    }
  }

  componentDidMount() {
    console.log(‘didMount‘)
  }

  render() {
    return (
      <div>
        Usual
      </div>
    )
  }
}
//IIHOC
import React from ‘react‘;

const iiHoc = WrappedComponent => class extends WrappedComponent {
    render() {
      console.log(this.state, ‘state‘);
      return super.render();
    }
}

export default iiHoc;

iiHoc return的组件通过继承,拥有了Usual的生命周期及属性,所以didMount会打印,state也通过constructor执行,得到state.usual。
其实,你还可以通过II:

渲染劫持

这里HOC里定义的组件继承了WrappedComponent的render(渲染),我们可以以此进行hijack(劫持),也就是控制它的render函数。栗子:

  //hijack-hoc
  import React from ‘react‘;

  const hijackRenderHoc = config => WrappedComponent => class extends WrappedComponent {
    render() {
      const { style = {} } = config;
      const elementsTree = super.render();
      console.log(elementsTree, ‘elementsTree‘);
      if (config.type === ‘add-style‘) {
        return <div style={{...style}}>
          {elementsTree}
        </div>;
      }
      return elementsTree;
    }
  };

  export default hijackRenderHoc;
  //usual
  @hijackRenderHoc({type: ‘add-style‘, style: { color: ‘red‘}})
  class Usual extends Component {
    ...
  }

我这里通过二阶函数,把config参数预制进HOC, 算是一种柯理化的思想。
栗子很简单,这个hoc就是添加样式的功能。但是它暴露出来的信息却不少。首先我们可以通过config参数进行逻辑判断,有条件的渲染,当然这个参数的作用很多,react-redux中的connect不就是传入了props-key 嘛。再就是我们还可以拿到WrappedComponent的元素树,可以进行修改操作。最后就是我们通过div包裹,设置了style。但其实具体如何操作还是根据业务逻辑去处理的...

我的应用场景

  • 通常我会通过高阶组件去优化之前老项目写的不好的地方,比如两个页面UI几乎一样,功能几乎相同,仅仅几个操作不太一样,却写了两个耦合很多的页面级组件。当我去维护它的时候,由于它的耦合性过多,经常会添加一个功能(这两个组件都要添加),我要去改完第一个的时候,还要改第二个。而且有时候由于我的记性不好,会忘掉第二个... 就会出现bug再返工。更重要的是由于个人比较懒,不想去重构这部分的代码,因为东西太多了,花费太多时间。所以加新功能的时候,我会写一个高阶组件,往HOC里添加方法,把那两个组件包装一下,也就是属性代理。这样新代码就不会再出现耦合,旧的逻辑并不会改变,说不定哪天心情好就会抽离一部分功能到HOC里,直到理想的状态。
  • 另一种情况就是之前写过一个组件A,做完上线,之后产品加了一个新需求,很奇怪要做的组件B跟A几乎一模一样,但稍微有区别。那我可能就通过II的方式去继承之前的组件A,比如它在didMount去fetch请求,需要的数据是一样的。不同的地方我就会放到HOC里,存储新的state这样,再通过劫持渲染,把不同的地方,添加的地方进行处理。但其实这算Hack的一种方式,能快速解决问题,也反映了组件设计规划之初有所不足(原因比较多)。

Container解决不了的时候甚至不太优雅的时候。其实大部分时候包一层Container组件也能做到差不多的效果,比如操作props,渲染劫持。但其实还是有很大区别的。比如我们现在有两个功能的container,添加样式和添加处理函数的,对Usual进行包装。栗子:

//usual
class Usual extends Component {

  render() {
    console.log(this.props, ‘props‘);
    return <div>
      Usual
    </div>
  }
};
export default Usual;
//console - Object {handleClick: function}  "props"
import React, { Component } from ‘react‘;
import Usual from ‘./usual‘;

class StyleContainer extends Component {

  render() {
    return (<div style={{ color: ‘#76d0a3‘ }}>
      <div>container</div>
      <Usual {...this.props} />
    </div>);
  }
}

export default StyleContainer;

import React, { Component } from ‘react‘;
import StyleContainer from ‘./container-add-style‘;

class FuncContainer extends Component {
  handleClick() {
    console.log(‘click‘);
  }

  render() {
    const props = {
      ...this.props,
      handleClick: this.handleClick,
    };
    return (<StyleContainer {...props} />);
  }
}

export default FuncContainer;

外层Container必须要引入内层Container,进行包装,还有props的传递,同样要注意包装的顺序。当然你可以把所有的处理都放到一个Container里。那用HOC怎么处理呢,相信大家有清晰的答案了。

const addFunc = WrappedComponent => class extends Component {
  handleClick() {
    console.log(‘click‘);
  }

  render() {
    const props = {
      ...this.props,
      handleClick: this.handleClick,
    };
    return <WrappedComponent {...props} />;
  }
};
const addStyle = WrappedComponent => class extends Component {

  render() {
    return (<div style={{ color: ‘#76d0a3‘ }}>
      <WrappedComponent {...this.props} />
    </div>);
  }
};
const WrappenComponent = addStyle(addFunc(Usual));

class WrappedUsual extends Component {

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

显然HOC是更优雅一些的,每个HOC都定义自己独有的处理逻辑,需要的时候只需要去包装你的组件。相较于Container的方式,HOC耦合性更低,灵活性更高,可以自由组合,更适合应付复杂的业务。当然当你的需求很简单的时候,还是用Container去自由组合,应用场景需要你清楚。

注意点(约束)

其实官网有很多,简单介绍一下。

  • 最重要的原则就是,注意高阶组件不会修改子组件,也不拷贝子组件的行为。高阶组件只是通过组合的方式将子组件包装在容器组件中,是一个无副作用的纯函数
    要给hoc添加class名,便于debugger。我上面的好多栗子组件都没写class 名,请不要学我,因为我实在想不出叫什么名了... 当我们在chrome里应用React-Developer-Tools的时候,组件结构可以一目了然,所以DisplayName最好还是加上。
  • 静态方法要复制
    无论PP还是II的方式,WrappedComponent的静态方法都不会复制,如果要用需要我们单独复制。
  • refs不会传递。 意思就是HOC里指定的ref,并不会传递到子组件,如果你要使用最好写回调函数通过props传下去。
  • 不要在render方法内部使用高阶组件。简单来说react的差分算法会去比较 NowElement === OldElement, 来决定要不要替换这个elementTree。也就是如果你每次返回的结果都不是一个引用,react以为发生了变化,去更替这个组件会导致之前组件的状态丢失。
 // HOC不要放到render函数里面

 class WrappedUsual extends Component {

  render() {
    const WrappenComponent = addStyle(addFunc(Usual));

    console.log(this.props, ‘props‘);
    return (<div>
      <WrappedComponent />
    </div>);
  }
}

使用compose组合HOC。函数式编程的套路... 例如应用redux中的middleware以增强功能。redux-middleware解析

const addFuncHOC = ...
const addStyleHOC = ...//省略

const compose = (...funcs) => component => {
  if (funcs.lenght === 0) {
    return component;
  }
  const last = funcs[funcs.length - 1];
  return funcs.reduceRight((res, cur) => cur(res), last(component));
};

const WrappedComponent = compose(addFuncHOC, addStyleHOC)(Usual);
关于注意点,官网有所介绍,不再赘述。链接

总结

高阶组件最大的好处就是解耦和灵活性,在react的开发中还是很有用的。
当然这不可能是高阶组件的全部用法。掌握了它的一些技巧,还有一些限制,你可以结合你的应用场景,发散思维,尝试一些不同的用法。

时间: 2024-10-27 16:11:46

高阶组件的相关文章

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

使用 react已经有不短的时间了,最近看到关于 react高阶组件的一篇文章,看了之后顿时眼前一亮,对于我这种还在新手村晃荡.一切朝着打怪升级看齐的小喽啰来说,像这种难度不是太高同时门槛也不是那么低的东西如今可不多见了啊,是个不可多得的 zhuangbility的利器,自然不可轻易错过,遂深入了解了一番. 概述 高阶组件的定义 React 官网上对高阶组件的定义: 高阶部件是一种用于复用组件逻辑的高级技术,它并不是 React API的一部分,而是从React 演化而来的一种模式. 具体地说,

Mixins 改成使用高阶组件调用

把组件放在另外一个组件的 render 方法里面, 并且利用了 {...this.props} {...this.state} 这些  JSX 展开属性 对比下2种代码: 原始方式: <!DOCTYPE html> <html> <head> <script src="../build/react.js"></script> <script src="../build/react-dom.js">

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

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

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函数中返回的 匿名函数 // 在设计模式中这种操作叫做 装饰器模式 // 高

高阶组件简介

组件:class Hello extends React.Component{ render(){ return <h2>hello imooc i love react&redux</h2> } } 组件本质其实就是一个函数. 高阶组件: function WrapperHello(Comp){ class WrapComp extends React.Component{ render(){ return( <div> <p>这是高阶组件特有的元

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

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

react:高阶组件wrappedComponent

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

修饰器&amp;高阶组件

一.修饰器 1.类的修饰 修饰器是一个函数,用来修改类的行为 function testable(target) { target.isTestable = true; } @testable class MyTestableClass { // ... } MyTestableClass.isTestable // true 注意: 修饰器函数的第一个参数,就是所要修饰的目标类 如果有多余参数,只能修饰器外面封装一层函数 function testable(isTestable) { retu

【转】react的高阶组件

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