Skip to content
This page has been auto-translated and may contain errors.View in English

Hooks

假如你的组件需要记住下拉菜单是否打开,或者需要在渲染后运行一些代码来与服务器同步数据。这两种情况都需要用到 Hook。你已经用过其中两个了:useState 让组件拥有记忆能力,useEffect 在渲染后运行代码来做 React 之外的事情。这两个是你用得最多的,其他内置 Hook 的用法也是一样的:你在组件内调用一个函数,它就把你接入 React 的某个功能。这个函数的名字总是以 use 开头,这个前缀就是 Hook 的全部定义。

常用内置 Hook 简览

你不需要在第一天就掌握所有 Hook,但知道有哪些工具可用很有帮助,这样当遇到问题时你就能认出合适的工具。以下是最常用的几个:

  • useRef 保存一个可变的值,在渲染之间保留这个值,不会触发新的渲染。它也是你获取 DOM 元素直接引用的方式,比如聚焦一个输入框。
  • useContext 从周围的 provider 读取共享数据,这样一个深层嵌套的组件就不用通过每一层都传递 props 了。
  • useReducer 通过 reducer 函数来管理状态,每次更新描述一个从当前状态到下一个状态的转换。当状态变化变得复杂到单个 useState 开始吃力的时候,这个很好用。
  • useMemo 在渲染之间缓存一个计算值,只在它的输入改变时才重新计算。
  • useCallback 对函数做同样的事,在渲染之间返回同一个函数实例,而不是每次都产生一个新的。

useMemouseCallback 会在后面有真正的性能故事要讲时再深入讨论。Refs and the DOM 会完整讲解 useRef。对于 useContextuseReducer,这章的内容是最深入的,有需要的时候就回来查看。现在的要点是这个模式:Hook 是你进入某个 React 功能的方式,它的名字告诉你这是一个 Hook。

Hook 的规则

Hook 有两条规则,React 依赖你遵守它们。

首先,在组件的顶层调用 Hook。不要在条件、循环或嵌套函数里调用它们。组件使用的每个 Hook 都应该在每次渲染时以相同的顺序运行。

jsx
function Profile({ isLoggedIn }) {
  // 错误:这个 Hook 只在某些时候运行
  if (isLoggedIn) {
    const [name, setName] = useState('')
  }

  // 正确:在顶层无条件地调用它
  const [name, setName] = useState('')
  // ...然后在你的逻辑里使用 isLoggedIn
}

其次,只在 React 函数组件或其他自定义 Hook 里调用 Hook。普通的 JavaScript 函数不能使用 Hook,因为 React 没有地方来附加这个状态。

编写自己的 Hook

假如多个组件都需要知道浏览器窗口的宽度。与其在每个组件里复制同样的 useStateuseEffect 代码,不如把这个逻辑封装一次:

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

现在任何组件都能调用 const width = useWindowWidth() 并获得一个会随窗口大小变化而更新的值。useWindowWidth 是一个自定义 Hook:一个调用其他 Hook 的函数。就是这样一个模式,因为它是一个函数,你可以给一段有状态的逻辑起个名字,把它写在一个地方,然后在多个组件间重用它。use 前缀是把它标记为 Hook 的方式,这样 React 的工具和读你代码的人都能认出来。自定义 Hook 把你已有的 Hook 打包成可重用的东西;它们不会给 React 增加新的能力。

关于什么时候用哪个 Hook,有几条经验法则。当你需要保存一个值(比如定时器 ID 或前一个 prop),这个值不应该在改变时触发重新渲染,或者当你需要直接获取一个 DOM 节点时,用 useRef。当某个值(比如当前主题或已登录用户)需要传给多层嵌套的组件,而在每一层都通过 props 传递会导致要改动很多和它无关的组件时,用 useContext。当一个状态有多个互相关联的子值在一起更新,或者下一个状态依赖具体的某个动作而不只是一个新值时,用 useReducer,因为 reducer 把这个转换逻辑保存在一个函数里,而不是分散在多个 setState 调用里。只有在你已经观察到真实的性能下降时,比如昂贵的计算在每次渲染时重新运行,或者某个函数身份破坏了 memoization(比如在 React.memo 的子组件或其他 Hook 的依赖数组里),才用 useMemouseCallback

自定义 Hook 的命名和提取也有自己的模式。名字总是以 use 开头,这样 React 的工具和任何读代码的人都能判断出它可以调用其他 Hook。当你发现同样的 useStateuseEffect 组合,或其他任何 Hook,在多个组件里重复出现时,这就是提取的信号:把这个逻辑拉到自己的 use 前缀函数里,让它返回组件真正需要的东西——一个值、一个设置函数,或两者——然后从每个组件里调用这个函数,而不是重复这些代码。

这些规则不是随意的。React 不知道你的状态变量叫什么名字;它通过调用的顺序来追踪组件里的 Hook。第一个 Hook、第二个 Hook、第三个 Hook,以此类推。下一次渲染时,它会按同样的顺序走过同样的列表,把每个调用匹配到上一次设置的状态。只要顺序在每次渲染时都相同,匹配就能保持。把一个 Hook 放在条件后面,条件一旦翻转列表就会移位,React 就会把错的状态配给错的调用,组件就会以诡异的方式崩溃。在顶层无条件地调用,这才能保证顺序稳定。

useMemouseCallback 值得单独说一下。它们是手动优化:你在告诉 React 保留一个计算值或函数,这样它就不用在每次渲染时重复计算或交出新实例。它们会增加一些复杂度,很容易在不需要的地方滥用。React Compiler 越来越多地通过在构建时自动 memoize 这类东西,从而让它们不必要,所以手动使用它们应该是你有意识地做的决定,是在你测量过真实成本的地方,而不是对每个值的反射性反应。

JunoHook 让组件使用 React 的功能 Hook 是一个名字以 use 开头的函数,它把你的组件接入 React 的某个功能,比如内存或副作用。你已经遇过 useStateuseEffect 了,其他的也是一样的形式。最重要的是:在组件的顶部调用 Hook,永远不要在 if 或循环里,这样它们在每次渲染时都以相同的方式运行。
JunoHook 让组件使用 React 的功能 除了 useStateuseEffect,你还有 useRef 用来保存在渲染间存活但不触发重新渲染的值,useContext 来跳过 prop 钻取,useReducer 用来做结构化的状态转换,还有 useMemo/useCallback 来缓存。在组件或另一个 Hook 的顶层调用它们,始终以相同的顺序。当你发现自己在重复相同的有状态逻辑时,把它拉到自定义 Hook 里,一个调用其他 Hook 的 use 函数,然后重用它。
JunoHook 让组件使用 React 的功能 React 通过调用顺序而不是名字来匹配 Hook 和它们的状态,所以顺序在每次渲染时都必须相同。这一个事实正是两条规则的来源。自定义 Hook 是组合:一个函数调用 Hook,所以相同的排序规则也适用于它们内部。还有,把 useMemo/useCallback 看作有意识的、测量过的优化,因为 React Compiler 正在稳步让手动版本变成不必要。

下一步:Thinking in React,这是关于组件、props、状态和副作用如何联合起来把设计变成 UI 的过程。