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

Hook

当组件需要记住下拉菜单是否打开,或者需要在渲染后运行代码来与服务器同步时,这些都是 Hook 的用途。你已经用过两个了:useState 让组件拥有记忆,useEffect 在渲染后运行代码来与 React 外部交互。这两个是你最常用的,其他内置 Hook 也遵循同样的模式:你在组件内调用一个函数,它就会让你接入 React 的某个功能。这个函数的名字总是以 use 开头,这个前缀就是 Hook 的全部定义。

常见内置 Hook 速览

你不需要在第一天就掌握所有 Hook,但了解有哪些工具可用会很有帮助,这样当遇到问题时你就能识别出正确的工具。以下是最常用的几个:

  • useRef 持有一个在多次渲染间保持不变的可变值,并且不会触发新的渲染。当你需要直接访问 DOM 元素时(比如让一个输入框获得焦点),也可以用它。
  • useContext 从周围的 provider 读取共享数据,这样深层嵌套的组件就能获取到值,而不需要你逐层传递 props。
  • useReducer 通过 reducer 函数来管理状态,每次更新都描述从当前状态到下一个状态的转换。当状态变化变得复杂到单个 useState 难以应付时,这是个很好的选择。
  • useMemo 在多次渲染间缓存一个计算结果,只在输入改变时才重新计算。
  • useCallback 对函数做同样的事,在多次渲染间返回同一个函数实例,而不是每次都创建一个新的。

useMemouseCallback记忆化章节有更深入的讨论,那里会讲性能优化的故事。Ref 和 DOM 会全面介绍 useRefContext 则会从本章的总结深入到 provider 和共享状态。对于 useReducer,这一章是最深入的讲解,需要复习时可以回来查看。现在的重点是这个模式: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 把你已有的 Hook 组合成可重用的东西,不会给 React 增加新的能力。

选择哪个 Hook 有几条经验法则。当你需要持有一个值(比如定时器 ID 或上一个 prop)而不想在它改变时触发重新渲染,或者需要直接获取 DOM 节点时,选择 useRef。当一个值(比如当前主题或已登录用户)需要传给多层嵌套的组件,逐层通过 props 传递会导致触碰那些本不关心它的组件时,选择 useContext。当一个状态有多个子值一起更新,或者下一个状态取决于一个具体的操作而不是单纯的新值时,选择 useReducer,因为 reducer 会把转换逻辑集中在一个函数中,而不是散落在多个 setState 调用里。只有在你实际观察到了性能问题时才选择 useMemouseCallback,通常是昂贵的计算在每次渲染时重复运行,或者函数标识破坏了其他地方的记忆化(比如 React.memod 的子组件或另一个 Hook 的依赖数组)。

命名和提取自定义 Hook 有自己的模式。名字总是以 use 开头,这样 React 的工具和读代码的人都知道可以调用其他 Hook。当你发现相同的 useStateuseEffect 组合,或者其他任何 Hook,在多个组件中出现时,这就是提取的信号:把逻辑拉到自己的 use 前缀函数中,从各个组件调用它,而不是重复这些代码。自定义 Hook 实战 会进一步讲解 Hook 应该返回什么,以及如何设计它的选项。

这些规则不是武断的。React 不知道你的状态变量名;它通过调用顺序来追踪一个组件中的 Hook。第一个 Hook、第二个 Hook、第三个 Hook,以此类推。下次渲染时,它会按相同的顺序遍历列表,把每次调用和上次设置的状态匹配起来。保持顺序在每次渲染都一致,匹配就能保持。把一个 Hook 放在条件后面,当条件翻转时列表就会移位,React 会把错误的状态配到错误的调用,组件就会以令人困惑的方式破坏。在顶层无条件地调用,就能保证顺序稳定。

useMemouseCallback 值得单独说一下。它们是手动优化:你告诉 React 保持一个计算值或函数,这样它就不用在每次渲染时重做工作或分配新实例。它们给你增加了复杂度,而且很容易在它们帮助不大的地方使用。React Compiler 越来越多地通过在构建时对这类东西自动记忆化来让它们变得不必要,所以把手动调用它们当作有意识的做法,是你测量过真实成本之后的选择,而不是对每个值的本能反应。

JunoHook 让组件使用 React 功能 Hook 是一个名字以 use 开头的函数,它让你的组件接入像记忆或副作用这样的 React 功能。你已经见过 useStateuseEffect,其他的也遵循同样的模式。

要记住的主要点:在组件顶部调用 Hook,永远不要在 if 或循环里调用,这样它们就能在每次渲染时以相同的方式运行。

JunoHook 让组件使用 React 功能 除了 useStateuseEffect,还有 useRef 用于在渲染间保持值而不触发重新渲染,useContext 用于跳过逐层传 props,useReducer 用于结构化的状态转换,以及 useMemo/useCallback 用于缓存。

在组件或另一个 Hook 的顶层调用它们,总是以相同的顺序。

当你发现自己在重复有状态的逻辑时,把它拉到一个自定义 Hook 中,一个调用其他 Hook 的 use 函数,然后重用它。

JunoHook 让组件使用 React 功能 React 通过调用顺序而不是名字来匹配 Hook 和它们的状态,所以顺序在每次渲染都必须一致。这个单一事实是两条规则的来源。

自定义 Hook 是组合:一个调用 Hook 的函数,这就是为什么相同的排序规则也适用于它们内部。

useMemo/useCallback 当作有意识的、经过测量的优化来对待,因为 React Compiler 在稳步推进手写版本变得不必要的这一天。

接下来:用 React 思考,在那里组件、props、状态和副作用会聚在一起,形成一个把设计转化为 UI 的过程。