资讯专栏INFORMATION COLUMN

写给自己的React HOC(高阶组件)手册

W4n9Hu1 / 2229人阅读

前言

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

这种设计模式可以复用在React组件中的代码与逻辑,因为一般来讲React组件比较容易复用渲染函数, 也就是主要负责HTML的输出.

高阶组件实际上是经过一个包装函数返回的组件,这类函数接收React组件处理传入的组件,然后返回一个新的组件.
注意:前提是建立在不修改原有组件的基础上.

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

具体的实施

流程如下:

找出组件中复用的逻辑

创建适用于上方逻辑的函数

利用这个函数来创建一个组件

enjoy it

找出组件中复用的逻辑

在实际开发中, 这种逻辑的组件非常常见:

组件创建

向服务器拉取数据

利用数据渲染组件

监听数据的变化

数据变化或者触发修改的事件

利用变化后的数据再次渲染

组件销毁移除监听的数据源

首先我们来创建一个生产假数据的对象来模拟数据源:

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 => ({name})) 
    )
  }
}

ReactDOM.render(, 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 => (
{name}
)) ) } } ReactDOM.render(, document.getElementById("root"));

这里我把redner中原来渲染的span标签改为了div标签,虽然这是一个小小的变化但是请你脑补这是两个渲染结果完全不同的组件好了.

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

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

一个返回组件的函数

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

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

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

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

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

class A extends React.Component {

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

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

  render() {
    return (
      this.state.data.map(name => ({name}))
    )
  }
}

class B extends React.Component {

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

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

  render() {
    return (
      this.state.data.map(name => (
{name}
)) ) } }

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

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

    constructor(props){

      super(props);

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

    }

    render(){
      return  {/* 通过 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(){
      
    }
  }
}
利用高阶组件来创建组件

经过上面的思考,实际上已经完成了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 (
    
  );
}
HOC是函数!利用函数来最大化组合性

因为HOC是一个返回组件的函数,只要是函数可以做的事情HOC同样可以做到.
利用这一点,我们可以借用在使用React之前我们就已经学会的一些东西.

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

function HighLevelHoc(content) {
  return function (Wrap, className) {
    return class extends React.Component {
      render() {
        return (
          {content}
        )
      }
    }
  }
}

class Test extends React.Component {
  render() {
    return (
      

{this.props.children || "hello world"}

) } } const H1Test = HighLevelHoc("foobar")(Test, 1); ReactDOM.render(, document.getElementById("root"));

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

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

function Demo(props) {
  return (
    
{props.children}
) } const App = DemoHoc(Demo); ReactDOM.render(, 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 ;
}
记得复制静态方法

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

class Demo extends React.Component{
  render(){
    return (
      
{this.props.children}
) } } Demo.echo = function () { console.log("hello world"); } Demo.echo();// 是可以调用的 // -------- 定一个类提供一个静态方法 function DemoHoc(Wrap) { return class extends React.Component{ render(){ return ( {"hello world"} ) } } } const App = DemoHoc(Demo); // ----- HOC包装这个类 App.echo(); // error 这个静态方法不见了

解决方式

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

function DemoHoc(Wrap) {

  const myClass = class extends React.Component{
    render(){
      return (
        {"hello world"}
      )
    }
  }

  myClass.echo = Wrap.echo;

  return myClass;
}

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

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

  const myClass = class extends React.Component{
    render(){
      return (
        {"hello world"}
      )
    }
  }

  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 
{this.state.message}
} }

我们使用一个 HOC 包裹它:

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

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

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

const ref = React.createRef();

class App extends React.Component {

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

  render() {
    return (
      
{/* 点击按钮相当于执行echo */}
) } }

但是当你点击按钮试图触发子组件的事件的时候它不会起作用, 系统报错没有 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  // 2. 我们接收到 props 中被改名的 ref 然后绑定到 ref 上
    }
  }
  return React.forwardRef((props,ref)=>{ // 1. 我们接收到 ref 然后给他改名成 forwardedRef 传入到props中
    return 
  })
}

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

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/109694.html

相关文章

  • 动手写个React高阶组件

    摘要:作用是给组件增减属性。如果你的高阶组件不需要带参数,这样写也是很的。那么需要建立一个引用可以对被装饰的组件做羞羞的事情了,注意在多个高阶组件装饰同一个组件的情况下,此法并不奏效。你拿到的是上一个高阶组件的函数中临时生成的组件。 是什么 简称HOC,全称 High Order Component。作用是给react组件增减props属性。 怎么用 为什么不先说怎么写?恩,因为你其实已经用...

    xiaokai 评论0 收藏0
  • 【译】TypeScript中React高阶组件

    摘要:原文链接高阶组件在中是组件复用的一个强大工具。在本文中,高阶组件将会被分为两种基本模式,我们将其命名为和用附加的功能来包裹组件。这里我们使用泛型表示传递到的组件的。在这里,我们定义从返回的组件,并指定该组件将包括传入组件的和的。 原文链接:https://medium.com/@jrwebdev/... 高阶组件(HOCs)在React中是组件复用的一个强大工具。但是,经常有开发者在...

    wizChen 评论0 收藏0
  • React深入】从Mixin到HOC再到Hook

    摘要:与继承相比,装饰者是一种更轻便灵活的做法。它只是一种模式,这种模式是由自身的组合性质必然产生的。对比原生组件增强的项可操作所有传入的可操作组件的生命周期可操作组件的方法获取反向继承返回一个组件,继承原组件,在中调用原组件的。 导读 前端发展速度非常之快,页面和组件变得越来越复杂,如何更好的实现状态逻辑复用一直都是应用程序中重要的一部分,这直接关系着应用程序的质量以及维护的难易程度。 本...

    fox_soyoung 评论0 收藏0
  • React函数子组件(FaCC)和高阶组件(HOC)

    摘要:高阶函数我们都用过,就是接受一个函数然后返回一个经过封装的函数而高阶组件就是高阶函数的概念应用到高阶组件上使用接受一个组件返回一个经过包装的新组件。灵活性在组合阶段相对更为灵活,他并不规定被增强组件如何使用它传递下去的属性。 在接触过React项目后,大多数人都应该已经了解过或则用过了HOC(High-Order-Components)和FaCC(Functions as Child ...

    elliott_hu 评论0 收藏0
  • React入门0x018: 高阶函数组件(HOC)

    摘要:总结其实,这个和的思想有很大的渊源,不推荐继承,而是推荐组合,而就是其中的典范。比如我们写了两个个高阶组件,一个是,一个是,组件就可以随意的在和之间随意切换,而不需要改动组件原有代码。 0x000 概述 高阶函数组件...还是一个函数,和函数组件不同的是他返回了一个完整的组件...他返回了一个class!!! 0x001 直接上栗子 照常,先写个App组件,外部传入一个theme ...

    QLQ 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<