理解 React Hooks

引言

medium.com/@dan_abramov

这个星期 Sophie Alpert 和我在 React Conf 演讲了 "Hooks" 的提案(proposal) 接着是

Ryan Florence 的深度潜入:youtu.be/dpw9EHDh2bM

我强烈建议去看一下这个开幕式主题演讲去了解这个问题。我们尝试用 Hooks 的提案去解决。因为花费一小时看视屏也是很大的时间投入,所以我决定在下面分享几个 Hooks 的想法。

注意 :Hooks只是个 react 的实验提案,你不需要马上去学习它。另外,这个文章主要包含我的个人观点 ,不一定代表整个 react 团队的立场。

为什么是 Hooks ?

我们通过组件和自顶向下的数据流,能把大的 UI 拆分成小的,独立的,可以重用的小片。但是,我们常常不愿意打散复杂的组件,因为复杂组件里面逻辑是有状态的,不能被抽取成函数或者其他组件。 这就是有时候说的 react 不能把复杂问题拆分成“独立的问题”的意思。

动画,表单处理,连接到外部数据源,和很多其它希望从组件去做的事情的用例非常常见。但是当我们想用单独的组件去解决这些用例的时候,我们最后往往会因为以下面的这些原因而放弃:

  • 巨大的组件 很难重构和测试
  • 重复的逻辑 在不同的组件和生命周期方法里面
  • 复杂的模式 例如 render props 和高阶组件

Hooks 是最好的武器去解决这些问。Hooks 让我们能把组件内部逻辑组织成可以重用的小单元。

twitter @sunil Pai

twitte @Pavel Prichodko


Hooks 在组件里面应用了 React 哲学显示的数据流和组合) ,而不是仅仅在不同的组件之间。 这就是为什么说 Hooks 是天生的贴合 React 组件模型的。

不像 render props 和 高阶组件的模式。Hooks 不会引入不必要的嵌套到你的组件树。它们也不会受 mixins缺点 影响。

即使你一开始是质疑的(和我一开始一样),但是我鼓励你去尝试一下这个提案,我认为你会喜欢它。

Hooks 会让 React 组件臃肿吗?

在我们看下面的 Hooks 细节之前,你可能担心我们只是给 React 加了 Hooks 这个概念。

虽然学起来一开始会有些费劲,但是最后你会发现是值得的。

如果 react 社区也拥抱了 Hooks 提案,那么在写 react 应用的时候,它反而会减少你需要兼顾的核心概念。 Hooks 能让你总是使用函数,而不是频繁的在函数,类,高阶组件和 render props 之间切换。

在安装的大小方面,对 Hooks 的支持,让 React 只增长约 1.5 kb (min+zip)。这并不是很大,而且极大可能使用 Hooks 还会减少你的包的大小。因为使用 Hook 的代码, 比同等的使用 classes 实现的代码更好压缩 。下面的例子有一点极端,但是它高效的解释了原因(点开去看全部的线程)

twitter @BOOlean

Hooks 的提案没有什么特别重大的变化。你现有的代码依旧还能工作,即使在你很早写的组件里面使用 Hooks 。实际上,这就是我们所推荐的 —-不做任何重大的重写。这样也不影响之后在任何核心的代码里面使用 hooks。 另外,如果你测试 16.7 alpha 后,发现什么问题,可以在 Hook 提案bug 报告里面给我们提供反馈, 我们会很感激。

准确的说,什么是 hooks ?

为了理解 Hooks,我们需要看上一步,思考代码重用。

今天,有很多方式去重用 React 应用里面的逻辑。我们可以写简单的函数,然后调用它们去计算。我们也可以写组件(那些本身可能是函数或者 classes)。组件更强大,但是它们不得不渲染一些 UI,这样让他们变得不方便去分享非可视化的逻辑。这就是为什么最后我们会选用复杂的模式,比如 render props 和 高阶组件。 如果只是同一个普通的方式去重用代码而不是用各种各样的模式,React 会不会变的更简单?

函数似乎是一个完美的代码重用机制。把逻辑在函数之间移动几乎不费力。然而,在函数里面没有本地的 React 状态(state)。在不重构代码,或者引入一个观察者模式这种抽象概念的情况下,你不能从 class 组件中抽取出行为,比如 “监听窗口大小和更新状态” 或者“随时间更新值” 。React 追求的是简单,而提到的这两种抽取方式都影响我们喜欢 React 的这一特性。

Hooks 完美的解决了这类难题。Hooks 能让你在函数里面使用 react 的特性 —通过一个简单的函数调用。 React 提供了一些内置的 Hooks,把 React 里面 state, lifecycle 和 context 这些内部构建块对外暴露。

因为Hooks 是规范的 JavaScript 函数,你可以把 React 提供的内置 Hooks 组合成你自己的 “自定义 Hooks” 。这让你把复杂的问题转成一行的程序,并且在应用或者社区之间分享。

twitter @Laurie Voss

注意自定义 Hooks 不是技术上的 React 特性。要写出原生的自己的 Hooks 是需要依照 Hooks 的设计方式来的。

看一些代码

比如我们想让组件监听当前窗口的宽度,(例如:为了在有限的可视区域根据宽度呈现不同的内容)

在今天你可以用几种方式去写你的代码。它们包括写 class ,然后设置不同的生命周期,或者如果你想在不同组件之间重用的话,可能甚至需要抽象出 render prop 或者高阶组件。但是我认为没有什么比下面的方式更激动人心:code link

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

如果你看了这段代码,它其实准确的做了描述的事情。 我们在组件里面获取 window 的宽度。如果它变化, React 重新渲染。这就是是 Hooks 的目标 —  让组件取定义事情,即使它包含状态(state)和 副作用(side effects)。

让我们看看如何能实现定制的 hooks。我们用 React 的本地 state 去获取当前窗口宽度,使用 side effect 去设置 state, 当窗口大小变化。

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

从上面你可以看到, React 内置的 Hooks 像 useState 和 useEffect 作为一个基础的构建块提供服务,我们可以直接在组件里面重用。 或者我们可以组合成自定义的 Hooks, 比如 useWindowWidth。使用自定义的 Hooks 就像使用 React 内置 API 一样顺畅。

你可以学到更多的内置 Hooks,从文档 里面

Hooks 是完全封装的 — 每次你调用 Hook,它都可以在当前执行的组件里面获取独立的状态(local state)。虽然这个例子有点特殊(窗口的宽度对所有组件都一样),但是没有关系,

不过这就是让组件更强大的原因。它们没有分享 state 的方式 —  但是有方式去分享状态逻辑。我们不想破坏嵌套最外层的数据流!

每个 Hooks 都包含一些本地状态(local state)和 副作用(side effects)。你可以把数据在多个 hooks 之间传递,就像在普通的函数之间那样传递。他们也可以接受参数和返回值,因为他们就是 JavaScript 函数。

这里有一个使用 Hooks 实验的 React 动画库。codeSandbox

请注意这个 demo 的源代码,惊人的动画是通过在同样的渲染函数里面,用几个自定义 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 最初的目的,你可以翻墙看更强大的交互 debug 工具。

twitter @Dan Abramov

在 Hooks 之间传递数据的能力非常适合表现动画,数据订阅,表单管理,和其他有状态的抽象概念。 不像 render props 和高阶组件。Hooks 不会在渲染树里面创建一个“虚拟的层次结构”。 它更像一个扁平的“小内存”列表,绑定在组件上,而不是多出来的一层。

那么 Classes 呢 ?

定制 Hooks,在我们的观点里面,是 Hooks 提案里面最有吸引的部分。但是为了定制 Hook 能工作,React 需要给函数提供一个方式去定义 state 和 side effects,这就是像 useState useEffects 这些内置 Hooks 做的。你可以通过文档去学习。

它证明内置的 Hooks 不只可以用来创建自定义的 Hooks。它也能用于满足自定义普通的组件。它像 state 的一样给我们提供了所有的必要的特性。这就是为什么我们希望在未来 Hooks 能成为最主要的方式去定义 React 组件。

我们没有废弃 classes 的计划。在 Facebook,我们有成百上千的 class 组件,所以跟大家一样,我们不打算重写他们。但是如果 React 社区拥抱 Hooks,建议两种不同的方式去写组件是不合理的。要提供更灵活的方式去抽取,测试和重用代码, Hooks 就需要能覆盖所有类( classes ) 的用例。这就是为什么 Hooks 描绘了我们对 React 未来的愿景。

但是为什么 React Hooks 不是“魔法” ?

你可能会因为 Hooks 的规则 而惊讶

Hooks 必须有最高的被调用优先级,这一点是不同寻常的。即使你可以,你很可能不想在条件判断里面定义state。例如,你也不能在 class 的条件判断里面定义 state 。谈论 react 超过四年的用户,我还没听到他(她)们抱怨过这一点。

这设计至关重要的开启了自定义 Hooks, 而不需要引入额外的语法和其他坑。一开始我们会觉得不熟悉,但是考虑到它提供的特性,这个交易是值得的。如果你不同意,我鼓励你在实践中去玩一下,去看你的感受是否会发生改变。

我们已经在生产中使用 Hooks 一个月了,去看是否工程师被这些规则困惑。我们发现人们只需要几个小时就能熟悉它。就我个人而言,我承认这些规则一开始让我感觉很糟糕,但是我很快适应了。这经历就跟我对 react 的第一印象一样。(你会立刻爱上 React吗 ?我不会爱上它,直到我第二次尝试)

既然在 Hooks 的实现中没有魔法。就像 Jamie 指出,它看起来非常像下面的代码

// Originally written by @jamiebuilds

let hooks = null;

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

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

我们在每个组件里面保留了 Hooks 的列表,并且移动到下一个元素,无论这个 hooks 是否被使用。多亏了 Hooks 的规则,每次渲染,他们的顺序都是一样的。所以每次调用的时候,能给组件提供正确的state。不要忘记, React 不需要做任何事情去知道哪个组件正在渲染— React 只是调用组件。

Rudi Yardley的这个文章 包含了一个漂亮的可视化解释。

可能你会好奇 React 把 hooks 的 state 存储在哪里。答案是它被放在跟 React 为 classes 保存 state 一样的地方 。React 有一个内部更新队列,这其实是任何 state 的真实的源头,不论你如何定义你的组件。

Hooks 不依赖代理或者取值方法,而这些在现在的 JavaScript 库里面是很普通的。

所以 Hooks 可以说是相比于一些流行的方法用了更少的黑魔法去解决相似的问题。

Hooks 跟调用 array.push 和 array.pop 用到的魔法类似 。(对数组来说,调用顺序也很重要)

Hooks 设计没有跟 React 绑定。实际上,在这个提案被发布的前几天,不同的人想出了几种跟 Hooks API 的一样的实验实现方案,主要是适用于 vue,web 组件和甚至普通的 JavaScript 函数。

最后,如果你是存粹的函数式编程爱好者,对依赖可变的状态作为实现细节,感到不安。你可能会发现处理 Hooks 能够用代数这种存粹的方式去实现,才能让人满意(如果 JavaScript 支持的话。 当然 React 在内部总是依赖可变的状态  —  所以你不需要。

你是更加务实还是教条主义(如果你是), 我希望至少这些理由当中的一个能够让你理解。最重要的是,我认为 Hooks 能让我们不费力构建组件,并且创建更好的体验。这就是我个人对 Hooks 很感兴趣的原因。

分享喜欢的事物,而不是炒作

如果 hook 对你来说还是不那么吸引,我完全能够理解。 我仍然希望你可以在小的实验项目里面尝试它,看是否会改变你的观点。 无论你是否已体验了用 Hooks 解决的问题,或者你有一个不同的解决方案的思路,请让我们在 RFC上知道!

如果我的介绍让你很兴奋,至少有一点好奇,那就太棒了。我只有一个要求。 有很多现在学习 React 的 folks(人们),如果我们匆忙写教程和定义才出来几天的某个特性的最佳实践,会他(她)们感到困惑。毕竟关于 hooks ,仍然有很多甚至我们 React team 自己都不是很清晰的事情。

如果你担心 Hooks 是不稳定的,请明确提出,它们只是实验的提案,包含官方文档 的 link。 我们会保持这个提案包含任何最新的变化。我也会画一些时间让它变的好理解,在这里很多问题已经被回答。

当你告诉那些没有跟你一样兴奋的人的时候,请保持礼貌。 如果你看到了误解,你可以分享一些额外的信息去解释,如果其他人对这个问题是开放的。 但是任何变化都是会让人害怕的。作为社区,我们会尽最大的努力去帮助人,而不是疏远他们。如果我(或者 React team 任何人)没有遵守这点,请直接指出。

下一步

可以查看下面的文档去学习它:

Hooks 还在很早期的阶段,但是我们很希望听到你们的反馈。你可以直接到 RFC 文档,我们会尽最大的努力在twitter 上保持会话。

请让我知道是否有什么不清晰的,我们很高兴去聊你的担心,谢谢阅读。

编辑于 2019-02-22

文章被以下专栏收录

    掘金翻译计划,可能是世界最大最好的英译中技术社区,最懂读者和译者的翻译平台。内容覆盖区块链、人工智能、Android、iOS、前端、后端、设计、产品和其他 等领域,以及各大型优质官方文档及手册,读者为热爱新技术的新锐开发者。期待你的加入 https://github.com/xitu/gold-miner