Making Sense of React Hooks
(原文发布于 2018 年 10 月 31 日)
这周, Sophie Alpert 和我在 React Conf 上介绍了 "Hooks" 提案,之后 Ryan Florence 又进行了深入的介绍:
视频地址
我强烈建议大家去看看这个开场主题演讲,了解我们试图用 Hooks 提案解决的问题。不过一个小时也是巨大的时间投入,所以我决定在下面分享一些关于 Hooks 的想法。
注意:Hooks 是 React 的实验性提案。你不需要现在了解他们。另请注意,这篇文章包含了我自己的观点,并不一定反映 React 团队的立场。
为什么用 Hooks ?
我们知道组件和自上而下的数据流帮助我们将大型的 UI 分解成小型,独立,可重用的部分。但是我们经常无法进一步分解复杂的组件,因为逻辑是有状态的,无法提取到函数或者其他组件。这就是人们说 React 不让他们 “分开关注” 的意思。
这种情况非常常见,包括动画,表单处理,连接到外部数据源以及我们希望的许多在组件中执行的其他操作。当我们尝试只使用组件解决这鞋问题时,我们通常会这么解决:
- 难以重构和测试的巨大的组件。
- 在不同的组件和生命周期方法中的重复的逻辑。
- 像 render props 和高阶组件一样的复杂的模式。
我们认为 Hooks 是解决所有这些问题的最好机会。Hooks 让我们将组件内部的逻辑组织成可重用的隔绝单元:
在一个组件中 Hooks 符合 React 的理念(明确的数据流和组成),而不仅仅是组件之间。这就是为什么我觉得 Hooks 很适合 React 的组件模型。
和 render props 或者高阶组件的模式不同,Hooks 不会在组件树中引入不必要的嵌套。它们也没有 mixins 的弊端。
即使你的第一反应是发自肺腑的(像我第一次那样),我也鼓励你不带偏见地去尝试这个提案,我想你会喜欢它的。
Hooks 会使 React 更冗余吗?
在我们仔细了解 Hooks 之前,你可能会担心我们只是用 Hooks 在 React 中添加了更多的概念。这是一种正常的顾虑。我认为学习它们肯定会有短期的认知成本,但是最终的结果将是相反的。
如果 React 社区拥抱了 Hooks 提案,它将减少编写 React 应用程序时需要处理的概念数量。 Hooks 让你总是使用函数而不必在函数(function),类(classes),高阶组件(higher-order components)和 render props 之间切换。
就实现大小而言,Hooks 支持只增加了 React ~1.5kB(min + gzip)。虽然这不多,但是使用 Hooks 可能会减少你的包的大小,因为使用 Hooks 的代码比使用类(classes)的代码更容易缩小。下面这个例子比较极端但是它有效地演示了为什么(点击查看整个例子):
(原文这里挂掉了)
Hooks 提案不包含任何破坏性升级。当你在新写的组件中使用 Hooks 的时候你现有的代码将继续工作。实际上这正是我们所推崇的——不做任何重大的改写!在任何关键代码中采用 Hooks 都是一个好主意。尽管如此,如果你尝试使用 16.7 alpha 版本之后向我们提供关于 Hooks 的反馈并报告一些错误,我们会非常感激。
Hooks 究竟是什么?
要理解 Hooks,我们需要退一步然后思考代码重用。
今天,有很多方法可以在 React 应用中重用逻辑。我们可以写简单的方法并调用它们来计算某些东西。我们也可以编写组件(它们本身可以是函数或是类)。组件功能更强大,但是他们需要渲染一些 UI。这使得它们不便于共享非可视逻辑。这就是为什么最终我们会得到像渲染 props 和高阶组件的复杂的模式。如果有一种常用的方法替代这些实现代码重用,React 会不会更简单?
函数看上去是代码重用的完美机制。函数之间逻辑转移花费最少的消耗。但是函数不能在它里面包含本地 React 状态(local React state)。如果你不重构代码或者引入像 Observables 这样的抽象,你无法从类组件中抽取出“监视窗口大小且更新状态”或“随时间变化值”这样的事件。这两种事件会伤害我们所喜欢的 React 的简洁性。
Hooks 正好解决了这个问题。 Hooks 允许你在函数中使用 React 的功能(像 state)——通过执行单个函数调用。React 提供了一些内置的 Hooks,它们暴露了 React 的“构建快”:状态,生命周期和上下文。
由于 Hooks 是常规的 Javascript 函数,因此你可以将 React 提供的内置 Hooks 组合到你自己的“自定义 Hooks”中。这使你可以将复杂的问题转换成单行,并在整个应用程序或者 React 社区中分享它们:
请注意,自定义 Hooks 在技术上不是 React 的功能。你自己的 Hooks 的实现得益于 Hooks 设计的方式。
看代码!
假设我们想给组件订阅当前窗口的宽度(例如在窄视窗上显示不同的内容)。
现在你有好几种方法去编写这种代码。它们包括编写类,设置一些生命周期方法,如果要在组件之间重用它,甚至可以提取出 render props 或者高阶组件。但我认为没有什么比这更好:
// MyResponsiveComponent.js
function MyResponsiveComponent() {
const width = useWindowWidth(); // Our custom Hook
return (
<p>Window width is {width}</p>
);
}
// useWindowWidth.js
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
});
return width;
}
Examples from "Making Sense of React Hooks"
如果你读了这段代码,它完全照它说的执行。我们在组件中使用窗口高度,当它变化时 React 重新渲染组件。这就是 Hooks 的目标——使组件真正声明,即使它们包含状态和副作用。
让我们看看如何实现这个自定义 Hook。我们使用 React 的本地状态 来保存当前窗口宽度,窗口大小改变时用一个副作用去设置这个状态。
如你所见, 像 useState 和 useEffet 这些内置的 React Hooks 充当基本构建块。我们可以直接在我们的组件中使用它们,或者我们可以将它们组合成像useWindowWidth那样的自定义 Hooks。使用自定义 Hooks 感觉就像使用 React 内置 API 一样顺手。
你可以通过这个介绍了解更多的内置 Hooks 相关内容。
Hooks 是完全封装的——每一次你调用 Hook,它在当前正在执行的组件中获得隔离的本地状态。这对于这个特定的例子无关紧要(窗口宽度在所有组件中都相同),但这是 Hooks 如此强大的原因。它们不是一种分享状态的方式 — 而是一种分享状态逻辑的方式。我们不想破坏自上而下的数据流!
每个 Hook 可能包含一些本地状态和副作用。你可以像平时在函数之间那样在多个 Hooks 之间传递数据。它们可以接受参数并返回值,因为它们是 Javascript 函数。
这是一个实验 Hooks 的 React 动画库的例子:
https://codesandbox.io/embed/...
请注意,演示代码中通过一个渲染函数中使用多个自定义 Hooks 来传递值实现交错动画。
https://codesandbox.io/s/ppxnl191zx
(如果你想了解更多关于这个例子,看看这个教程。)
虽然这不是 Hooks 的主要目标,它们还为强大的交互调试工具打开了大门:
Hooks 之间传递数据的能力使它们非常适合展示动画,描述数据,表单管理,以及其他有状态的抽象。不像 render props 或者 高阶组件, Hooks 不会在你的渲染树上创建一个“错误的层级”。它们更像是一个连接到组件的“存储单元”的平面列表。没有额外的层。
那么类怎么办?
在我们看来,自定义 Hooks 是 Hooks 提案中最吸引人的部分。但是为了使自定义 Hooks 工作,React 得提供函数去声明状态和副作用。这就是 useState 和 useEffect 这样的内置 Hooks 让我们做的。你可以在文档中了解它们。
事实证明,这些内置 Hooks 不仅在创建自定义 Hooks 的时候有用。它们也足以定义一般的组件,因为它们为我们提供了像 state 这样的所有必要的功能。这就是为什么我们希望 Hooks 在未来可以成为定义 React 组件的主要方式。
我们没有打算弃用类。在 Facebook,我们有成千上万的类组件,和你一样,我们无意重写它们。但是如果 React 社区拥抱 Hooks,用两种不同的推荐方法来编写组件是没有意义的。Hooks 可以覆盖类的所有用例,同时在提取,测试和重用代码方面提供更大的灵活性。这就是为什么 Hooks 代表了我们对 React 未来的愿景。
Hooks 不是魔术吗?
你可能会对 Hooks 的规则 感到惊讶。
虽然必须在顶层调用 Hooks 是不常规的,不过即使可以你估计也不希望在某个条件下定义状态。举个例子,你不能在类中有条件地定义状态,在与 React 用户交流的四年中我从来没有听到过他们对此有任何怨言。
这种设计对于启用自定义 Hooks 而不引入额外的语法杂质或其他陷阱至关重要。我们意识到最初的陌生感,但是我们认为这种代价是值得的。如果你不同意,我建议你在练习中使用它,看看它是否会改变你的感受。
为了了解开发者是否会对这些规则困惑,我们已经在生产中使用了一个月 Hooks。我们发现在实践中人们会在几个小时内习惯它们。就我个人而言,我承认起初我对这些规则也“感觉不舒服”,但是我很快就克服了它。这次经历很像了我对 React 的第一印象。(你马上就爱上 React 了吗?第二次使用时我才开始喜欢它。)
请注意,Hooks 的实现中也没有“魔法”,如 Jamie 指出 的那样,它看起来与下面非常相似:
我们保留了每个组件的 Hooks 列表,并在每次使用 Hooks 的时候移动到列表中的下一个。由于 Hooks 的规则,在每个 render 中它们的顺序都是相同的,因此我们可以为组件的每个调用提供正确的状态。不要忘记 React 不需要做任何特殊的事情来知道哪个组件正在渲染 — React 只调用你的组件。
(Rudi Yardley 的这篇文章 包含了一个很好的可视化解释!)
或许你还想知道 React 把 Hooks 的状态放在哪里。答案是它们保存在 React 为类保存状态的地方。不论你怎么定义你的组件,React 有一个内部更新队列,这是所有状态的来源。
Hooks 不依赖于现代 Javascript 库中常见的代理(Proxies)或者 getters。所以讲道理 Hooks 没有那些流行的解决类似问题的途径神奇。我会说 Hooks 和调用 array.push
, array.pop
一样神奇(调用顺序也很重要!)
Hooks 的设计与 React 无关。事实上,在提案发布后的前几天,针对 Vue,Web 组件甚至纯 Javascript 函数不同的人都想出了相同的 Hooks API 的实验性实现方式。
最后,如果你是一个纯粹的函数式编程主义者并且对 React 的依赖可变状态作为实现细节感到不自在,你可能会对使用代数效果纯粹地处理 Hooks 的方式感到满意(如果 Javascript 支持它们)。当然 React 一直在内部依赖可变状态 — 目的就是你不必这么做。
无论你是从务实的角度还是教条的角度来考虑(如果你有的话),我希望这些理由中至少有一个能说服你。如果你很好奇,Sebastian(Hooks 的作者)在这篇关于 RFC 的 回复中 也回应了这些和其他问题。最重要的是,我认为 Hooks 帮助我们用更少的精力构建组件,创造了更好的用户体验。这就是我个人对 Hooks 感到兴奋的原因。
传播爱,不炒作
如果你还没被 Hooks 吸引,我完全可以理解。但我还是希望你能尝试一个小的项目,看看是否会改变你的观点。无论你是遇到了 Hooks 解决不了的问题,或是你有其他的解决方案,请通过 RFC 告诉我们!
如果我的介绍让你兴奋了,或者至少有一点好奇,那就太好了!我只有一个请求。现在有很多人学习 React,如果我们忙于编写教程并为几乎刚出来没几天的功能发布最佳实践,他们会感到困惑。Hooks 中有一些东西甚至对 React 团队中的我们来说都不是很清楚。
如果你在 Hooks 不稳定时创建任何有关 Hooks 的内容,请特别提及它们是实验性提案,并带上包含 官方文档 的链接。我们会及时更新所有提案的更改。我们也花了不少精力使它更全面,在那里很多问题都已经得到解答了。
当你和其他不像你那么兴奋的人交流时,请保持理性。如果你发现别人误解了它,可以在对方开放的时候分享一些额外的信息。不过任何的改变都是可怕的,作为一个社区,我们应该尽力帮助人们,而不是疏远他们。如果我(或者是 React 团队中的任何其他人)未能遵循这个建议,请联系我们!
下一步
查看 Hooks 提案的文档以了解更多信息:
- 介绍 Hooks(动机)
- Hooks 一览(演练)
- 创建你自己的 Hooks(这是有趣的部分)
- Hooks 常见问题(你的问题很有可能在那里得到答案!)
Hooks 仍然处于早期阶段,但是我们很高兴听到你么所有人的反馈,你可以前往 RFC ,但我们也会尽力跟上 Twitter 上的对话。
如果有不清楚的地方,请告诉我,我很乐意与你聊聊你的疑虑。谢谢你的阅读!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。