理解 React Hooks
最初发表于Medium
本周,Sophie Alpert和我在 React Conf 上提出了“Hooks”提案,随后Ryan Florence进行了深入探讨:
我强烈建议您观看这个开场主题演讲,了解我们试图通过 Hooks 提案解决的问题。然而,即使是一个小时的演讲也需要投入大量的时间,所以我决定在下面分享一些关于 Hooks 的想法。
注意:Hooks 是 React 的一个实验性提案。您现在不需要了解它们。另请注意,本文仅代表我的个人观点,并不一定反映 React 团队的立场。
为什么使用 Hooks?
我们知道,组件和自上而下的数据流可以帮助我们将大型 UI 组织成小的、独立的、可复用的部分。然而,我们通常无法进一步分解复杂的组件,因为这些逻辑是有状态的,无法提取到函数或其他组件中。有时,人们说 React 不允许“分离关注点”,就是这个意思。
这些情况非常常见,包括动画、表单处理、连接外部数据源以及我们希望通过组件实现的许多其他功能。当我们尝试仅使用组件来解决这些用例时,通常会得到以下结果:
- 庞大的组件难以重构和测试。
- 不同组件和生命周期方法之间的重复逻辑。
- 复杂的模式,如渲染道具和高阶组件。
我们认为 Hooks 是解决所有这些问题的最佳方案。Hooks让我们将组件内部的逻辑组织成可复用的独立单元:
Hooks 将 React 的理念(显式数据流和组合)应用于组件内部,而不仅仅是组件之间。正因如此,我认为 Hooks 与 React 组件模型非常契合。
与渲染 props 或高阶组件等模式不同,Hooks 不会在组件树中引入不必要的嵌套。它们也不会受到mixins 的弊端的影响。
即使你一开始会有这种反应(就像我一开始那样!),我也鼓励你认真尝试一下这个方案。我想你会喜欢的。
Hooks 会让 React 变得臃肿吗?
在我们详细了解 Hooks 之前,你可能会担心我们只是用 Hooks 给 React 添加了一些概念。这种批评很合理。我认为,虽然学习 Hooks 肯定会在短期内带来认知成本,但最终结果却恰恰相反。
如果 React 社区接受 Hooks 提案,它将减少你在编写 React 应用程序时需要处理的概念数量。Hooks让你可以始终使用函数,而不必在函数、类、高阶组件和渲染 props 之间不断切换。
就实现大小而言,Hooks 支持仅使 React 体积增加约 1.5kB(min+gzip)。虽然这不算多,但采用 Hooks也有可能减少你的打包体积,因为使用 Hooks 的代码往往比使用类的等效代码压缩效果更好。下面这个例子有点极端,但它有效地说明了原因(点击查看完整帖子):
Hooks 提案不包含任何重大更改。即使您在新编写的组件中采用了 Hooks,您现有的代码也能继续工作。事实上,这正是我们推荐的——不要进行任何大规模的重写!最好先在任何关键代码中采用 Hooks。不过,如果您能试用 16.7 alpha 版本,并向我们提供有关Hooks 提案的反馈并报告任何错误,我们将不胜感激。
Hooks 到底是什么?
为了理解 Hooks,我们需要退一步思考代码重用。
如今,React 应用中有很多逻辑复用的方法。我们可以编写简单的函数并调用它来计算。我们也可以编写组件(组件本身可以是函数或类)。组件功能更强大,但它们必须渲染一些 UI。这使得它们不便于共享非可视化逻辑。这导致了我们最终使用诸如渲染 props 和高阶组件之类的复杂模式。如果 React 中只有一种通用的代码复用方法,而不是那么多,那岂不是更简单?
函数似乎是代码复用的完美机制。在函数之间移动逻辑所需的工作量最少。然而,函数内部不能包含本地 React 状态。如果不重构代码或引入像 Observables 这样的抽象,你就无法从类组件中提取诸如“监视窗口大小并更新状态”或“随时间对值进行动画处理”之类的行为。这两种方法都会损害我们喜欢的 React 简洁性。
Hooks 正好解决了这个问题。Hooks 让你可以通过一个函数调用来使用 React 的特性(比如状态)。React 提供了一些内置的 Hooks,暴露了 React 的“构建块”:状态、生命周期和上下文。
由于 Hook 是常规的 JavaScript 函数,你可以将 React 提供的内置 Hook 组合成你自己的“自定义 Hook”。这样,你就可以将复杂的问题简化为一行代码,并在你的应用程序或 React 社区中共享:
请注意,自定义 Hooks 从技术上来说并非 React 特性。自定义 Hooks 的可能性源于 Hooks 的设计方式。
给我看一些代码!
假设我们想要让组件订阅当前窗口宽度(例如,在窄视口上显示不同的内容)。
目前,有几种方法可以编写此类代码。它们包括编写一个类、设置一些生命周期方法,甚至提取一个 render prop 或一个高阶组件(如果你想在组件之间复用它)。但我认为没有什么比这更好了:

如果你读过这段代码,就会发现它确实做到了它所说的。我们在组件中使用窗口宽度,如果宽度发生变化,React 就会重新渲染组件。这就是 Hooks 的目标——让组件真正具有声明性,即使它们包含状态和副作用。
让我们看看如何实现这个自定义 Hook。我们会使用 React 本地状态来保持当前窗口宽度,并在窗口调整大小时使用副作用来设置该状态:

如上所示,React 内置的 Hooks(例如useState和useEffect)是基础的构建块。我们可以直接从组件中使用它们,也可以将它们组合成自定义 Hooks(例如useWindowWidth)。使用自定义 Hooks 就像使用 React 内置 API 一样顺畅。
您可以从此概述中了解有关内置 Hooks 的更多信息。
Hook 是完全封装的——每次调用 Hook 时,它都会在当前正在执行的组件内获得独立的本地状态。对于这个特定示例来说,这并不重要(所有组件的窗口宽度都相同!),但这正是 Hook 如此强大的原因。它们不是一种共享状态的方式,而是一种共享状态逻辑 的方式。我们不想破坏自上而下的数据流!
每个 Hook 可能包含一些本地状态和副作用。你可以在多个 Hook 之间传递数据,就像通常在函数之间传递数据一样。由于它们是JavaScript 函数,因此它们可以接受参数并返回值。
以下是使用 Hooks 进行实验的 React 动画库的示例:
请注意,在演示源代码中,交错动画是通过在同一个渲染函数中通过几个自定义 Hook 传递值来实现的。

(如果您想了解有关此示例的更多信息,请查看本教程。)
Hook 之间能够传递数据,这使得它们非常适合用于表达动画、数据订阅、表单管理以及其他有状态的抽象。与渲染 props 或高阶组件不同,Hook 不会在渲染树中创建“虚假的层级结构”。它们更像是附加到组件上的扁平“内存单元”列表。没有额外的层级。
那么课程怎么样?
我们认为,自定义 Hook 是 Hooks 提案中最吸引人的部分。但为了使自定义 Hook 能够正常工作,React 需要为函数提供声明状态和副作用的方法。而这正是useState和useEffect等内置 Hook 所实现的功能。您可以在文档中了解它们。
事实证明,这些内置 Hook 不仅可用于创建自定义 Hook,它们也足以用于定义一般的组件,因为它们提供了所有必要的功能,例如状态。这就是为什么我们希望 Hook 在未来成为定义 React 组件的主要方式。
我们没有计划弃用类。在 Facebook,我们有成千上万个类组件,和你们一样,我们无意重写它们。但是,如果 React 社区拥抱 Hooks,那么有两种不同的组件编写方式就毫无意义了。Hooks 可以覆盖类的所有用例,同时在代码提取、测试和复用方面提供更大的灵活性。这就是为什么 Hooks 代表了我们对 React 未来的愿景。
但是 Hooks 不是很神奇吗?
您可能对Hooks 规则感到惊讶。
虽然 Hook 很少需要在顶层调用,但即使可以,你也可能不想在条件语句中定义状态。例如,你也不能在类中以条件语句的方式定义状态,而且在与 React 用户交流的四年多时间里,我从未听到过任何关于此的抱怨。
这种设计对于启用自定义 Hook 至关重要,因为它不会引入额外的语法噪音或其他陷阱。我们理解用户最初可能会感到不熟悉,但我们认为这种权衡是值得的,因为它能够提供各种功能。如果您不同意,我鼓励您在实践中尝试一下,看看这是否会改变您的感受。
我们已经在生产环境中使用 Hooks 一个月了,看看工程师是否会对这些规则感到困惑。我们发现,在实践中,人们在几个小时内就能习惯它们。我个人承认,一开始我也觉得这些规则“不对劲”,但我很快就适应了。这段经历与我对 React 的第一印象如出一辙。(你是不是立刻就喜欢上了 React?我直到第二次尝试才喜欢上。)
请注意,Hooks 的实现也没有什么“魔法”。正如 Jamie指出的那样,它看起来与此非常相似:

我们为每个组件维护一个 Hook 列表,每当使用 Hook 时,都会跳转到列表中的下一个。得益于 Hook 规则,它们的顺序在每次渲染时都相同,因此我们可以为每次调用提供正确的组件状态。别忘了,React 无需执行任何特殊操作即可知道哪个组件正在渲染——React就是调用你组件的那个。
(Rudi Yardley 的这篇文章包含一个很好的视觉解释!)
你可能想知道 React 将 Hooks 的状态保存在哪里。答案是,它保存在 React 保存类状态的位置。React 有一个内部更新队列,它是任何状态的真实来源,无论你如何定义组件。
Hook 不依赖于现代 JavaScript 库中常见的代理或 getter。因此,可以说 Hook 的魔力不如一些解决类似问题的流行方法。我认为 Hook 的魔力大概相当于调用array.push和array.pop(调用顺序也很重要!)。
Hooks 的设计并非与 React 绑定。事实上,在提案发布后的最初几天里,不同的人针对 Vue、Web Components 甚至普通的 JavaScript 函数提出了相同的 Hooks API 的实验性实现。
最后,如果你是一位函数式编程的纯粹主义者,并且对 React 依赖可变状态作为实现细节感到不安,那么你可能会觉得用纯粹的代数效应(如果 JavaScript 支持的话)来处理 Hooks 会更令人满意。当然,React 内部一直依赖于可变状态——正是为了让你不必这么做。
无论你是从务实的角度还是教条的角度(如果你真的这么想的话),我希望这些理由至少有一个是合理的。如果你感兴趣的话,Sebastian(Hooks 提案的作者)也在RFC 的评论中回应了这些以及其他一些担忧。最重要的是,我认为 Hooks 让我们能够更轻松地构建组件,并创造更好的用户体验。这就是我个人对 Hooks 感到兴奋的原因。
传播爱,而不是炒作
如果您仍然觉得 Hooks 不太吸引人,我完全理解。我仍然希望您能在一个小项目中尝试一下,看看这是否会改变您的看法。无论您是否遇到过 Hooks 解决的问题,或者您有其他解决方案,请在 RFC 中告诉我们!
如果我的分享能让你感到兴奋,或者至少让你有点好奇,那就太好了!我只想拜托你一件事。现在有很多人在学习 React,如果我们匆忙地为一个才发布几天的功能写教程并宣扬最佳实践,他们会感到困惑。关于 Hooks,有些事情即使对我们 React 团队来说也还不太清楚。
如果您在 Hooks 不稳定的情况下创建任何关于它的内容,请明确指出这是一个实验性提案,并包含指向 官方文档 的链接。我们会及时更新提案的任何变更。我们也花费了不少精力使其更加全面,因此很多问题已经在那里得到了解答。
当你与那些不如你热情的人交谈时,请务必保持礼貌。如果你发现误解,如果对方愿意,你可以分享额外的信息。但任何改变都是可怕的,作为一个社区,我们应该尽力帮助人们,而不是疏远他们。如果我(或 React 团队的任何其他成员)没有遵循这个建议,请告诉我们!
后续步骤
查看 Hooks 提案的文档以了解更多信息:
Hooks 仍处于早期阶段,但我们非常期待听到大家的反馈。您可以将其发送至RFC,我们也会尽力在 Twitter 上关注大家的讨论。
如果您有任何不清楚的地方,请告诉我,我很乐意解答您的疑问。感谢您的阅读!
文章来源:https://dev.to/dan_abramov/making-sense-of-react-hooks-2eib