[译]认识React Hooks

300 阅读13分钟

原文地址:medium.com/@dan_abramo…

这周,Sophie Alpert 和我在React Conf大会上提出了Hooks的提案。引用来自 Ryan Florence的一段话: 我强烈建议大家去看一下PPT去了解我们想通过Hooks这个提案去解决什么问题。但是考虑到会花费大家很多时间,所以我 准备先分享一些关于hooks的想法。

提示:hooks目前任然是一个实验性的提案,你不需要立即去学习它。同时,这篇文章里面的观点是我个人的观点,并不代表是 React团队的官方观点。

为什么需要Hooks

我们知道组件以及自顶向下的数据流帮我们将一个大的UI组件拆分为小的、独立的、可复用的模块。但是,我们经常无法进一 步分解复杂的组件,因为组件逻辑是有状态的,并且无法提取到一个单独的函数或其他组件中。这就是有时候大家说React不让 他们去做逻辑拆分的原因。

像这些case很常见,包括动画、表单处理、连接外部数据源等其他我们想在组件内部做的事情,当我们想单独在组件中解决这些 问题的时候,我们通常的一个解决方案是:

  • 写成一个大组件,但是难以重构和测试
  • 在不同的组件和生命周期中维护重复的逻辑
  • 使用复杂的模式,比如render props或者higher-order component(高阶组件)

我们认为Hooks是解决这些问题最好的方式。Hooks可以让我们重新组织组件内部的代码逻辑,将其拆分成可复用的独立单元。

Hooks运用了React组件内部的哲学(明确的数据流和组合的思想),而不仅仅是另一种组件。这就是为什么我觉得Hooks非常适合React组件模型的原因。

不像render props或者higher-order components的模式,Hooks不会在组件内部引入不必要的嵌套。也不用忍受mixins模式的缺点。

即使你的第一反应(如同我第一次听到这个提案一样),我鼓励你去努力尝试它,我认为你会喜欢上它的。

Hooks会导致React的体积变得庞大吗

在我们去探究Hooks的详情之前,你也许会担心我们因为Hooks给React增加了很多概念。这是一个很常见的想法。我认为,虽然学习它们肯定会花费短期的认知成本,但最终结果将相反。

如果React社区接受Hooks提议,它将减少编写React应用时需要花时间去理解的技巧和概念。Hooks让你可以一直使用函数式组件避免经常需要在函数式组件、class组件、高阶组件和render props中切换。

就实现规模而言,React为了支持Hooks仅仅增加了约1.5kB(min + gzip)。尽管数量不多,但采用Hooks可能还会减小bundle的大小,因为使用Hooks的代码比使用class的编译后产生的代码更少。

下面的示例有些极端,但可以有效地说明原因(单击以查看整个线程)

Hooks提案并没有包含任何破坏性变更。你现有的代码位还是会正常运行,即使你在新组件中采用Hooks进行开发。实际上,这也是我们建议的:不要去为了Hooks重构你现有的代码。一个好的方式是在需要重构的代码中采用Hooks。但是,如果你可以使用16.7 alpha版本然后给我们一些关于Hooks提案BUG报告的反馈,我们会非常感激。

到底什么是Hooks

为了理解Hooks,我们需要回过头来想一想关于代码复用的问题。

今天,我们在React应用中有很多方法复用代码逻辑。我们可以写一个简单的函数然后调用它。我们也可以写一个组件(可以是函数式组件或者class组件)。组件功能更加强大,但是它们必须的渲染一些UI。这使得它们不方便去复用非视觉的逻辑。这也是为什么我们最终有一些复杂的模式比如render props或者higher-order component。那么React有没有一个简单通用的方法去复用代码呢?

函数看起来是一个完美的实现代码复用的机制。我们只需要花费很少的精力就可以在不同的函数之间复用代码逻辑。但是,函数式组件没办法在内部维护状态。你没办法在不重新组织代码的情况下抽离class组件内部比如监听window尺寸变化然后更新state,或者是一个随着时间设置值得动画,以及引入一个抽象的概念比如观察者模式。这些方法都破坏了我们喜欢的React的简单性。

Hooks完美解决了这些问题。Hooks让你可以通过调用一个函数在函数式组件中使用React的特性(比如state)。React提供了一些内置的Hook,它们暴露了React的:状态,生命周期和上下文。

因为Hooks是一个普通的JS函数,所以你可以通过组合不同的内置Hooks实现自定义Hooks。这可以让你将一个复杂的问题简化为一行代码并且可以让你跨应用分享以及在React社区进行分享。

注意自定义Hooks并不是React的一个新技术。你可以很自然的根据Hooks的设计编写你自己自定义的Hooks。

来看一些代码吧

让我们来看看如何在组件内部监听当前window的宽度。(例如,根据不同宽度展示不同内容)

现在有很多方式实现这个功能。比如写一个class组件,在某些生命周期实现一个方法,甚至还可以抽离出一个render prop或者higher-order component,如果你想在不同组件中复用。但是我想没有什么比下面这张方式更加简洁了:

function MyResponsiveComponent() {
  const width = useWindowWidth();
  return (
  	<p>Window width is {width}</p>
  );
}

如果你阅读这段代码,你可以明确知道它做了什么。我们在组件内部用到了window的width,然后当组件rerender的时候width会更新。这就是Hooks的目的-使组件真正具有声明性,即使它们包含状态和副作用。

让我们看看该如何实现这个自定义的Hook。我们使用了state去保存当前window的width,然后我们使用useEffect在window的尺寸发生变化的时候更新这个state。

import { useState, useEffect } from 'react';

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;
}

就像你看到的,useState和useEffect这些内置Hooks是React基础功能。我们可以直接在组件中使用,也可以在自定义Hooks中进行组合使用,像useWindowWidth那样。使用自定义的Hooks就如同使用React内置的API一样。

你可以在这里查看更多关于内置Hooks的信息。

Hooks是完全封装的-每次在当前组件内部调用Hooks,都可以得到一个独立的本地状态。对于这个特定的例子它无关紧要(window的width对所有组件都一样)。但是这个特点让它变得非常强大。它们不是共享状态的方法,而是共享有状态逻辑的方法。我们不希望破坏自顶向下的数据流设计。

每个Hooks可能包含一些本地状态或者一些副作用。你可以在不同Hooks之间传递数据就如同在普通函数直接一样。它可以接受参数然后返回一些values因为它们就是一个普通的JS函数。

这是一个React动画库尝试使用Hooks的示例

注意这个demo的源代码中,仅仅在render函数中通过给几个自定义的Hooks传递一些数据就实现了这个惊人的动画。

const [{ pos1 }, set] = useSpring({ pos1: [0, 0], config: fast });
const [{ pos2 }] = useSpring({ pos2: pos1, config: slow });
const [{ pos3 }] = useSpring({ pos3: pos2, config: slow });

(如果你想更进一步了解这个例子,点击这个教程)

但这个不是Hooks的主要目的,还提供了创建更为强大的交互式调试工具提供了可能性。

Hooks之间能够传递数据这个特性非常适合用来实现动画、数据订阅、表单管理以及一些其他抽象状态的逻辑。不像render props或者higher-order component这种模式,Hooks不会在渲染树种创建这种虚假的层级。

它更像是一个关联组件的扁平化内存数据列表。没有额外的层级。

那我们还需要class吗?

自定义Hooks在我们看来是Hooks提案中最吸引人的一个特点。但是为了自定义Hooks可以正常工作,React需要提供一个方式去声明state和处理副作用。这也正是内置Hooks比如useState和useEffect做的事情。你可以点击链接查看更多关于内置Hooks的信息。

事实证明这些内置Hooks不仅仅可以用于创建自定义Hooks。它们通常也足以定义组件,因为它们为我们提供了所有必要的功能,例如状态。这也是为什么我们希望在未来Hooks可以成为首选的创建组件的方式。

我们并没有计划废弃class组件。在Facebook我们有数以万计的class组件,但我们并没有准备去重写他们。但是如果React社区拥抱Hooks,那么就没有道理推荐两种方式去写组件。Hooks可以覆盖类的所有用例,同时在提取,测试和重用代码方面提供更大的灵活性。这就是为什么Hooks代表了我们对React未来的愿景。

那Hooks有什么魔法吗?

你也许对Hooks的限制规则比较惊讶。

虽然必须在顶层调用Hooks是不寻常的,但即使你愿意,你也可能不想在条件中定义状态。比如你也没办法在class组件中根据条件定义state。而且过去4年我也从来没有听到用户抱怨这一点。

这个规则对实现自定义Hooks的同时没有引入额外的语法以及其他可能的BUG至关重要。我知道这种限制大家刚开始会觉得不适应,但这种交换是值得的。如果您不同意,我建议您在实践中尝试一下,看看是否会改变你的看法。

我们已经在生产中使用Hooks一个月了,以查看工程师是否对这些规则感到困惑。我们发现,实际上,人们会在几个小时就习适应了他们。就我个人而言,我也承认起初我对这些规则也“感觉不对”,但我很快就克服了。这次经历反映了我对React的第一印象(你第一次上手就习惯React吗?我直到第二次尝试才真正习惯这种模式。)

所以Hooks的实现并不是什么魔法。如Jamie指出的,它类似于这样:

let hooks = null;

export function useHook() {
  hooks.push(hookData);
}

function reactsInternalRenderAComponentMethod(component) {
  hooks = [];
  component();
  let hooksForThisComponent = hooks;
  hooks = null;
}

我们在每个组件内部维护了一个Hooks的列表,然后每次一个Hooks被调用的时候移动到下一个Hooks。因为这种规则,我们保证了组件每次渲染的时候Hooks的执行顺序是相同的。所以我们可以保证提供给组件的状态是准确的。不要忘记,React不需要做任何特殊的事情就可以知道哪个组件正在渲染-React就是在调用您的组件。

Rudi Yardley的文章对这个问题做出了很好的解释。

也许你会疑惑React是在什么地方保存state的。答案就是和class组件的state保存在相同的地方。无论你如何定义组件,React都有一个内部更新队列,该队列是任何状态的真实来源。

Hooks不依赖那些现代JavaScript的框架所使用的Proxies或者getters。所以Hooks在解决这些类似问题上相比其他流行的方案上并没有什么魔法。我想说Hooks就如同调用array.push和array.pop一样(不过调用顺序很重要!)

在React里面实现Hooks并不费劲。在提案发布后的前几天,不同的人提出了针对Vue,Web组件甚至是普通JavaScript函数的相同Hooks API的实验性实现。

最后,如果您是函数式编程的纯粹主义者,并且对React依赖于可变状态作为实现细节感到不安,则可能会感到满意的是,可以使用代数效应(如果JavaScript支持)以纯方式实现处理Hooks。当然React内部总是依赖可变状态-正因为如此,你没必要这样。

无论您是从务实的角度还是从教条的角度考虑(如果有的话),我希望至少有一种理由是有意义的。Sebastian(Hooks提案的作者)在对RFC的评论中也回答了这些以及其他问题。最重要的是,我认为Hooks让我们以更少的精力来构建组件,并创造更好的用户体验。这就是为什么我个人对Hooks感到兴奋。

传播爱,不要炒作

如果Hooks似乎仍然对你没有吸引力,我完全可以理解。我还是希望你可以创建一个小项目来尝试一下,然后看看是否会改变你的看法。无论你是否经历过Hooks无法解决的问题,还是有其他想法,请在RFC中告诉我们!

如果我确实让你兴奋或至少有点好奇,那就太好了!我只有一个请求。现在有很多人正在学习React,如果我们很仓促的对仅仅发布几天的新特性写教程以及声明最佳实践,很容易让他们感觉困惑。现在即使是React团队成员对Hooks的认识也并不是十分清晰。

如果你在Hooks还不稳定的情况下开发了任何代码,请在显眼的地方特别提醒着是一个实验性的提案并且添加官方文档链接。我们会在文档中更新这个提案的最新变动。我们还花了很多精力来保证文档内容全面,在那里已经回答了许多问题。

如果你向别人介绍Hooks的时候别人并没有像你这样感到兴奋,请多理解。如果你发现一些误解,可以在别人愿意接受的情况下分享其他信息。任何改变都是令人恐惧的,作为一个社区,我们应该尽力帮助人们,而不是疏远他们。如果我(或React团队中的其他任何人)没有遵循此建议,请致电我们!

下一步计划

可以通过查看我们关于Hooks提案的文档来了解:

Hooks仍处于初期阶段,但我们很高兴听到大家的反馈。您可以到RFC反馈,但我们也将尽可能回复Twitter上的信息。

如果你有任何不明白的地方请让我们知道,我们非常高兴和你沟通讨论。感谢你的观看。