React hooks 是名字以 use 开头的函数,让函数组件可以使用 React 的功能:useState 记住一个值,useEffect 在渲染后运行代码,useRef 保存一个 DOM 节点,useContext 读取共享数据。你在组件顶部调用它们,React 会在多次渲染之间保留它们的值。
这里有四个 hook 协同工作。useState 保存文本,useRef 指向输入框,让按钮可以聚焦它,useMemo 在查询内容变化之前一直保留过滤后的列表,useEffect 在每次查询内容变化的渲染之后打印日志。输入 an 并观察 Console:每按一次键打印一行,而且是在列表更新之后。
为什么会有 hooks
在 React 16.8 之前,需要 state 或生命周期代码的组件必须写成类。同一个功能的逻辑被拆散在 componentDidMount、componentDidUpdate 和 componentWillUnmount 中,要在组件之间共享就得用 mixin(它从来没法和 class 语法一起用)、高阶组件或 render props,而这些方式都会给组件树套上额外的层级。
// The class way: one feature, three methods
class Clock extends React.Component {
state = { now: new Date() };
componentDidMount() {
this.id = setInterval(() => this.setState({ now: new Date() }), 1000);
}
componentWillUnmount() {
clearInterval(this.id);
}
render() {
return <p>{this.state.now.toLocaleTimeString()}</p>;
}
}
有了 hooks,同一个功能集中在一处:effect 启动定时器,它的清理函数停止定时器。
隐藏时钟后,Console 会打印 timer stopped:Clock 离开页面时 React 运行了清理函数。再次显示它,又会启动一个新的定时器。类组件在 React 19 中依然可用,但新代码都使用 hooks。
全部内置 hook
React 19.2 按照 hook 给组件提供的能力对它们分组。前两组你每天都会用到,其余的在遇到特定问题时再用。
| Hook | 提供的能力 | 页面 |
|---|---|---|
useState | 一个在多次渲染之间保留的值,以及一个会触发重新渲染的 setter | useState |
useReducer | 由 reducer 函数和派发的 action 更新的 state | useReducer |
useContext | 父组件提供的值,无需传递 props | useContext |
useRef | 一个在多次渲染之间保留的可变容器,常用来保存 DOM 节点 | useRef |
useImperativeHandle | 决定父组件通过你的 ref 能看到什么 | forwardRef |
useEffect | 通常在绘制之后运行的代码:定时器、订阅、获取数据 | useEffect |
useLayoutEffect | 在 DOM 更新之后、绘制之前运行的代码 | useLayoutEffect |
useInsertionEffect | 在 layout effect 之前运行,供 CSS-in-JS 库使用 | |
useEffectEvent | 一个供 effect 调用、总能看到最新 props 和 state 的函数 | useEffect |
useMemo | 在多次渲染之间缓存计算结果 | useMemo |
useCallback | 在多次渲染之间缓存函数 | useCallback |
useTransition | 把更新标记为非紧急,并告诉你它是否仍在等待中 | useTransition |
useDeferredValue | 一个值的副本,在繁重的渲染中会滞后更新 | useDeferredValue |
useId | 一个在服务器和客户端上一致的唯一 id | useId |
useSyncExternalStore | 订阅存在于 React 之外的 store | useSyncExternalStore |
useDebugValue | 在 React DevTools 中为自定义 hook 显示标签 | |
use | 读取 promise 或 context,可以写在条件语句中 | use |
useActionState | 由表单 action 更新的 state,附带等待中标志 | useActionState |
useOptimistic | 在服务器响应之前先显示预期结果 | useOptimistic |
useFormStatus | 外层表单的等待状态(来自 react-dom) |
use、useActionState、useOptimistic 和 useFormStatus 随 React 19 推出,useEffectEvent 在 19.2 中进入稳定版。列表中的其他 hook 在 React 18 中也能使用。
state 会触发渲染,ref 不会
并不是每个 hook 都会触发渲染。useState 和 useReducer 会:改变它们的值,React 就会重新绘制组件。useRef 给你一个对象,你可以随意修改它的 current 字段,React 永远不会因此重新渲染。所以它适合存放定时器 id、上一次的值,或者页面上不显示的计数。
useReducer 保存购物车,useState 保存备注;两者变化时都会触发渲染,effect 每次渲染打印一行。effect 还修改了 commits.current,但这个修改本身从不触发渲染,所以日志不会无限循环。计数之所以在 effect 中更新而不是在组件主体中,是因为组件主体在渲染时不应该修改任何东西。
hook 的使用规则
每个 hook 都遵守两条规则:
- 只在组件的顶层调用 hook,不要在条件语句、循环或嵌套函数中调用。
- 只在函数组件或你自己的自定义 hook 中调用 hook。
React 并不是靠名字识别 hook 的,而是靠调用顺序,并且要求每次渲染时顺序都相同。放在 if 中的 hook 会让它后面的所有 hook 错位。hook 的使用规则页面展示了会出什么问题以及怎么修复。use 是唯一的例外:它可以在条件语句中调用。
你的第一个自定义 hook
当两个组件需要同样的有状态逻辑时,把它移到一个名字以 use 开头的函数中。这个函数可以调用任何其他 hook。每个调用它的组件都拥有自己的 state。
两个计数器运行的是同一个 hook,但点击其中一个不会影响另一个:自定义 hook 共享的是逻辑,不是 state。给 useCounter 加一个 decrement 函数,再加一个调用它的按钮,两个计数器会同时获得这个功能。更多模式(useToggle、useDebounce、useLocalStorage)见自定义 hook 页面。
该用哪个 hook
- 值变化时页面也要跟着变:用
useState,如果许多事件都更新同一个 state,用useReducer。 - 需要和 React 之外的东西打交道(定时器、订阅、请求):用
useEffect。 - 需要一个 DOM 节点,或者一个页面上不显示的值:用
useRef。 - 组件树深处的许多组件需要同一个值:用
useContext。 - 渲染变慢是因为某个计算,或者因为一个已记忆化的子组件重新渲染:用
useMemo和useCallback。React Compiler 可以替你加上这些记忆化,但手写依然有效,也依然很常见。 - 输入有卡顿,因为背后的更新太重:用
useTransition或useDeferredValue。
常见问题
React 中的 hooks 是什么?
hooks 是名字以 use 开头的函数,让函数组件可以使用 React 的功能:useState 用于 state,useEffect 用于副作用,useContext 用于 context,useRef 用于在多次渲染之间保留值又不触发渲染,等等。
React 为什么要加入 hooks?
在 hooks 出现(React 16.8)之前,state 和生命周期方法只存在于类组件中,复用有状态的逻辑需要用 mixin、高阶组件或 render props。hooks 把这些逻辑放进普通函数中,任何函数组件都可以调用,还能封装成自定义 hook 共享。
React 有多少个 hook?
React 19.2 的 react 包中大约有二十个内置 hook,另外 react-dom 中还有 useFormStatus。大多数应用日常只用到其中几个:useState、useEffect、useRef、useContext、useMemo 和 useCallback。
类组件中可以使用 hooks 吗?
不可以。hooks 只能在函数组件和其他 hook 中使用。不过类组件仍然可以和函数组件一起渲染,类组件也可以渲染使用了 hooks 的函数组件。
什么是自定义 hook?
你自己写的、名字以 use 开头并调用其他 hook 的函数。它让多个组件复用同一段有状态的逻辑,而每个调用它的组件仍然拥有各自独立的 state。