# 1. 前言

在组件化开发思想诞生之日起,如何优雅的实现组件逻辑复用便成了绕不开的话题,因为在程序员的思想中 Don't Repeat Yourself 已经根深蒂固,能复用的就要复用,不能复用的想方设法也要复用,毕竟:懒惰,才是推动科技进步的第一动力啊!🐶🐶🐶

言归正传,经过无数的 React 开发者不断的探索发现,在 React 中,想要优雅的实现组件逻辑复用,无外乎以下三种方式:

  • 高阶组件(HOC)
  • Render Props
  • 自定义 React Hooks

接下来,本文就对这三种方式进行逐一分析,分别探讨一下它们都是如何实现组件逻辑复用的。

# 2. 高阶组件(HOC)

高阶组件( Higher Order Components )在概念上沿袭了高阶函数( Higher-Order Function )。所谓高阶函数就是:接收函数作为输入,或者输出另一个函数的一类函数,就是高阶函数。

相应的,高阶组件指的就是参数为组件,返回值为新组件的函数。没错,高阶组件本质上是一个函数

下面是一个简单的高阶组件示例:

const withProps = (WrappedComponent) => {
    const targetComponent = (props) => (
        <div className="wrapper-container">
            <WrappedComponent {...props} />
        </div>
    );
    return targetComponent;
};

1
2
3
4
5
6
7
8
9

其中, withProps 就是一个高阶组件。那么高阶组件是如何实现逻辑复用的呢?

假设我们现在有这样一个需求场景:

组件要根据当前登录的用户身份是否合法来决定展示的内容,如身份合法,展示正常信息,不合法,展示不合法原因等。

这个场景可以说是非常非常常见的了,针对这个场景,常规的做法是:在组件内对用户身份进行判断,并根据判断结果展示相应的信息。咋一看,感觉做的没毛病,但是如果有几十个组件都需要对用户身份进行判断的话,那么就要把判断用户身份的逻辑写几十遍,而且这几十遍的代码散落在项目的各个地方,如果产品在后续迭代的过程中判断用户身份的逻辑发生了变化,那就得从项目中找到这几十处代码一一改动,啊这,想想都后脊发凉!!!

此时,你就不得不考虑如何对判断用户身份这段逻辑进行复用,将这段逻辑抽离到一处,然后其余地方复用该逻辑,这才是完美的解决方案。

OK,需求明确后,接下来我们就来看看如何用高阶组件的方式来实现这段判断用户身份的逻辑的复用,代码如下:

import checkUserAccess from './utils

const withCheckAccess = (WrappedComponent) => {
    // 这部分是通用的逻辑:判断用户身份是否合法
    const isAccessible = checkUserAccess()  

    // 将 isAccessible(是否合法) 这个信息传递给目标组件
    const targetComponent = (props) => (
        <div className="wrapper-container">
            <WrappedComponent {...props} isAccessible={isAccessible} />
        </div>
    );
    return targetComponent;
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

如上述代码所示,我们在高阶组件中定义了这段判断用户身份的通用逻辑,并且把判断结果作为属性传递到目标组件中,在目标组件中就可以直接获取到判断结果,并根据结果进行条件渲染。

之后,当我们再次需要为某个组件复用这层请求逻辑的时候,只需要直接用 withCheckAccess 包裹这个组件就可以了。那么包裹它的形式就是下面代码这样:

const EnhancedAComponent = withCheckAccess(Acomponent);
1

高阶组件不仅能够帮助我们简化逻辑的引入过程,还可以帮助我们规避掉逻辑变更带来的烦琐的修改步骤:假如这段 checkUserAccess 的逻辑是散落在项目的各个地方,,那么一旦 checkUserAccess 的判定规则需要修改,我们就得需要去修改所有用到的地方的代码;但现在, checkUserAccess 被抽离进了一个独立的高阶组件里,我们在高阶组件中的一次修改,将在所有被它处理过的组件中生效。

# 3. Render props

render props 也是 React 中组件逻辑复用的一种非常常见的做法,它的思想和高阶组件一样:都是把通用的逻辑提取到某一处。两者之间最大区别主要在于使用层面:高阶组件是用“函数”包裹“组件”,而 render props 是用“组件”包裹“函数”。

经过上一章节对高阶组件的分析,相信你对**高阶组件是用“函数”包裹“组件”**这句话应该有所体会了,高阶组件的本质是一个函数,这个函数「吃」进去原始组件,「吐」出来一个经过增强后的组件,看起来像是把组件用函数包装了一下,从而使得组件的功能更为强大。

那么, **render props 是用“组件”包裹“函数”**这句话又该如何理解呢?多说无益,我们直接来看例子,一个简单的 render props 可以是这样的:

import React from 'react'

const RenderChildren = (props) => {
  return ( <
    React.Fragment > {
      props.children(props)
    } <
    /React.Fragment>
  );
};
1
2
3
4
5
6
7
8
9
10

使用方式如下:

<RenderChildren>         
  {(props) => <p>我是 RenderChildren 的子组件</p>}       
</RenderChildren>

1
2
3
4

通过上面两段代码,我们可以看到: render props 的载体应该是一个React 组件,并且它的子组件需要以函数形式存在RenderChildren 本身是一个 React 组件,它可以包裹其他的 React 组件。一般来说,我们习惯于看到的包裹形式是标签包裹标签,但在 render props 这种模式下,它要求被 render props 组件标签包裹的一定是个函数,也就是所谓的「函数作为子组件传入」。这样一来, render props 组件就可以通过调用这个函数,传递 props ,从而实现和目标组件的通信了。

回到上一章节的例子中,如果用 render props 这种模式对判断用户身份的逻辑进行抽离的话,该怎么做呢?其实也很简单,代码如下:

import checkUserAccess from './utils

// 定义 render props 组件
const CheckAccess = (props) => {
  const isAccessible = checkUserAccess()
  // 将 isAccessible(是否合法) 这个信息传递给目标组件
  return <React.Fragment > {
      props.children({
        ...props,
        isAccessible
      })
    } <
    /React.Fragment>
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

使用方式如下:

<CheckAccess>
  {
    (props) => {
      const { isAccessible } = props;
      return <ChildComponent {...props} isAccessible={isAccessible} />
    }
  }
</CheckAccess>

1
2
3
4
5
6
7
8
9

可以看到, CheckAccess 组件会对由它包裹的所有子组件的 props 里注入一个 isAccessible 属性用来标识用户身份的判断结果。

# render props 的灵活之处

到这里,就衍生出一个问题:**高阶组件和 render props 都是把通用逻辑提取到某一处,那么这两种方式哪个更好呢?或者说更推荐用哪种呢?**在这里,我推荐你使用 render props ,原因是 render props 相较于高阶组件更为「灵活」。

怎么个灵活法呢?我说的灵活是在于对数据的处理上:在高阶组件中,目标组件对于数据的获取没有主动权,数据的分发逻辑全部收敛在高阶组件的内部;而在 render props 中,除了父组件可以对数据进行分发处理之外,子组件也可以选择性地对数据进行接收

如何理解这个灵活法呢?还是回到上面的例子中,假如有一个老旧的组件,别的组件都是用 isAccessible 这个属性名来标识用户身份是否合法,而这个老旧的组件使用 isValid 来标识的,那么为了让这个老旧的组件也能享受上逻辑复用的好处,假如我们使用的是高阶组件的方式来实现通用逻辑复用的,那我们就不得不改造已有的高阶组件,如下:

const withCheckAccess = (WrappedComponent) => {
  const isAccessible = checkUserAccess()
  const targetComponent = (props) => ( <
    div className = "wrapper-container" > {
      /* 添加 isValid 属性 */ } <
    WrappedComponent {
      ...props
    }
    isAccessible = {
      isAccessible
    }
    isValid = {
      isAccessible
    }
    /> <
    /div>
  );
  return targetComponent;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

我们不得不再添加一个 isValid 属性来支持这个老旧的组件,有一个这样的老旧组件我们就得加一个属性,这显示是不合理的,违反了开放封闭原则

那么,如果使用 render props 的话,这个问题就很好解决了,如下:

<CheckAccess>
  {
    (props) => {
      const { isAccessible } = props;
      return <ChildComponent {...props} isValid={isAccessible} />
    }
  }
</CheckAccess>

1
2
3
4
5
6
7
8
9

可以看到,父组件只管向组件的 props 里注入属性,而属性具体要怎么用由子组件自己去决定,如此以来,就算有多少个这样的老旧组件我们都不怕了。

# 4. 自定义 React Hooks

React Hooks 诞生之前,无论是高阶组件,还是 render props ,这两者的出现都是广大开发者在逻辑复用上的探索而来的,并不是 React 本身提供的逻辑复用方式,并且这两种方式并不是完美无缺的,它也有缺点,包括不限于:

  • 嵌套地狱问题,当嵌套层级过多后,数据源的追溯会变得十分困难

  • 较高的学习成本

  • props 属性命名冲突问题

  • ......

但是,自从 React Hooks 诞生之后, React 原生就提供的逻辑复用的最佳实践——自定义 React Hooks (opens new window)Hooks 能够很好地规避掉以上两种方式带来的弊端,比如说它不存在嵌套地狱,允许属性重命名、允许我们在任何需要它的地方引入并访问目标状态等。

如今,当我们想要去复用一段逻辑时,第一反应肯定不是高阶函数或者 render props ,而应该是自定义 React Hooks!!!

# 5. 总结

本篇文章盘点了在 React 中对组件逻辑复用的三种方式,并且对其分别进行了分析探讨,自从 React Hooks 诞生之后,自定义 React Hooks 便成了 React 组件逻辑复用的最佳实践,至于如何编写自定义 React Hooks ,后续文章我们再见!