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

媒介

HOC(高阶组件)是React中的一种构造代码的手腕,而不是一个API.

这类设想形式能够复用在React组件中的代码与逻辑,由于平常来讲React组件比较轻易复用衬着函数, 也就是重要担任HTML的输出.

高阶组件现实上是经由一个包装函数返回的组件,这类函数吸收React组件处置惩罚传入的组件,然后返回一个新的组件.
注重:条件是建立在不修正原有组件的基础上.

文字描述太隐约,借助于官方文档稍稍修正,我们能够越发轻松的明白高阶组件.

详细的实行

流程以下:

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

找出组件中复用的逻辑

在现实开辟中, 这类逻辑的组件异常罕见:

  1. 组件建立
  2. 向服务器拉取数据
  3. 应用数据衬着组件
  4. 监听数据的变化
  5. 数据变化或许触发修正的事宜
  6. 应用变化后的数据再次衬着
  7. 组件烧毁移除监听的数据源

起首我们来建立一个生产假数据的对象来模仿数据源:

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();
}
    原文作者:ASCll
    原文地址: https://segmentfault.com/a/1190000019246191
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞