[译] 理解 React Render Props 和 HOC

2,861 阅读8分钟

React 中 Render Props 和高阶组件的详细介绍

reactjs.org

如果你最近有在做 React 开发,你肯定有遇到像 HOCs 和 Render Props 这样的术语。在本文中,我们将深入探讨这两种模式,以便了解我们为什么需要它们,以及如何正确地使用它们来构建更好的 React 应用。

为什么我们需要这些模式?

React 提供了一种简单的代码复用方式,即组件。组件封装了很多东西,包括内容、样式和业务逻辑。理想情况下,在单个组件中,我们可以将 html、css 和 js 结合起来,所有的这些是为了一个目的,单一职责

提示:使用 Bit (Github),你可以组织和分享可复用的组件,这些组件可以从不同的项目和应用中被发现,分享和开发。这比重写组件或者维护一个大型库要快得多。试试看 :)

例子

假设我们正在开发一个电子商务应用程序。它与其他电子商务应用程序一样,向用户显示所有可购买产品,并且用户可以将任何产品添加到购物车。我们将从 API 获取产品数据,并将产品目录显示为卡片列表。

在这种情况下,React 组件可以像这样实现:

代码链接

对于我们的管理员,有一个管理门户,他们可以在其中添加或删除产品。在此门户中,我们从同一 API 获取产品数据,并以表格形式显示产品目录。

这个 React 组件可以像这样实现:

代码链接

有一件事很明显,这两个组件都实现了产品的数据获取逻辑。

继续深入,以下这些情况也可能出现:

  • 我们必须使用产品数据并以不同的方式显示它。
  • 从不同的 API 中获取产品数据(在用户的购物车页面中很有用),但数据的显示和我们在 ProductList 中的做法类似。
  • 我们必须从 localStorage 访问它,而不是从 API 获取数据。
  • 在产品目录表格中,需要使用具有不同操作的按钮而不是删除按钮。

如果我们为这些每一点都写个不同的组件,那么我们将要复制大量的代码。

获取数据和显示数据是两个独立的关注点。正如前面所说的,如果一个组件有一个责任会更好。

让我们重构第一个组件。它将接受产品数据为属性,并像之前一样把产品目录渲染成卡片列表。由于我们不需要组件状态和生命周期方法,我们把它转换成函数式组件。

它现在看起来是这样的:

ProductList.js (代码链接)

就像 ProductListProductTable 会是一个函数组件,它接收产品数据为属性,并把数据渲染到表的行中去。

现在让我们创建一个名为 ProductsData 的组件。它从 API 获取产品数据。数据的获取和状态的更新将和原先的 ProductList 组件一样。但是我们应该在这个组件的 render 方法中放入什么呢?

ProductData.js (代码链接)

如果我们只是简单的放入 ProductList 组件,那么我们就不能复用这个组件于 ProductTable。不管怎样,如果这个组件可以询问要渲染什么,那个问题就会得到解决。在一个地方,我们将告诉它要渲染 ProductList 组件,而在管理门户中,我们告诉它要渲染 ProductTable 组件。

这就是 Render Props 和 HOCs 发挥作用的地方。它们只是一类方法,即对于一个组件,会询问应该渲染什么内容。这进一步推动了代码的复用。

现在我们知道了为什么需要它们,让我们来看看如何使用它们。

Render Props

在概念层面理解 Render Props 非常简单。让我们忘掉 React 一会,然后看看原生 JavaScript 下的事情。

我们有一个计算两个数字之和的函数。起初我们只想要把结果记录到控制台。所以,我们这样设计函数:

但是,我们很快发现 sum 函数非常有用,我们需要在其他地方使用到它。因此,我们希望 sum 函数只提供结果,而不是将其记录到控制台,并让调用者决定如何使用结果。

它可以这么做:

代码链接

我们传给 sum 函数一个 fn 回调函数作为参数。然后 sum 函数计算结果并把结果作为参数调用 fn。通过这种方式,回调函数可以获得结果,并且可以自由地对结果进行任何操作。

这就是 Render Props 的本质。我们将通过使用这个模式来更清晰地认识它,所以让我们立刻把它用到我们现在面临的问题中去吧。

在这不是计算两个数字之和的函数,而是获取产品数据的组件 ProductsData。现在可以通过属性传递给 ProductsData 组件一个函数。然后 ProductsData 组件将获取产品数据,并将这些数据提供给以属性方式传递进来的函数。传递进来的函数现在可以对产品数据做任何它想做的事情。

在 React 中,它可以像这样实现:

代码链接

就像 fn 参数,我们有一个 render 属性,它将作为一个函数被传递。然后 ProductData 组件把产品数据作为参数调用这个函数。

因此我们可以以这种方式使用 ProductData 组件。

代码链接

正如我们所看到的 Render Props 是一种相当通用的模式。大部分事情都可以非常直接地完成。但这也是我们搬起石头砸自己的脚的原因:

避免嵌套的一种简单方法是把组件拆解成更小的组件,并将这些组件保存在单独的文件中。另一种方法是编写更多的组件并组合它们,而不是在 Render Props 中使用长函数。

接下来,我们将看下另一种流行的模式,它被称为 HOC。

高阶组件(HOC)

在这个模式中,我们定义了一个函数,该函数接受一个组件作为参数,然后返回相同的组件,但是添加了一些功能。

如果这听起来很熟悉,那是因为它类似于 Mobx 中广泛使用的装饰器模式。像 Python 这样的许多语言都内置了装饰器,JavaScript也很快就会支持装饰器。HOCs 很像装饰器。

比起用文字解释,用通过代码来理解 HOCs 会容易很多。所以让我们先来看代码。

代码链接

正如我们所看到的,数据获取和状态更新逻辑就和我们在 Render Props 所做的一样。唯一的变化就是组件类是位于函数内部。该函数接受一个组件为参数,然后在内部的 render 方法中渲染这个组件,但是会添加额外的属性。对于名称如此复杂的模式,实现起来相当简单,对吧?

代码链接

现在我们已经了解了为什么我们需要 Render Props,HOCs 以及我们如何实现它们。

还有一个问题:如何在 Render Props 和 HOCs 中进行选择?关于这个话题的文章已经有很多了,所以我现在不讨论这个话题。也许在我的下一篇文章中 :)

什么时候不要使用 Render Props — Kent C. Dodds

HOCs vs Render Props — Richard Kotze

结论

在本文中,我们了解了为什么需要这些模式,每个模式的本质和如何利用这些模式来构建高度可复用的组件。以上就是全部内容,希望你喜欢,请随意评论和提问。我很乐意交流 👏

2018 年 10 月更新:React hooks 已经在 alpha 版本中中发布。它们将消除编写类组件、HOCs 和 Render Props 的痛苦。我很快就会写一篇来解释,关注我的 TwitterMedium 或者订阅 我的时事通讯 来获取最新消息。

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏