React Hook
本文最后更新于 2025年9月11日 凌晨
useState
基础
1 | |
传入一个 initialState(也可以不传);返回一个 state 和用于更新 state 的函数 setState。在初始渲染期间,返回的 state 与传入的参数 initialState 相同。
1 | |
setState 函数用于更新 state,它接收一个新的 newState 并将组件的一次重新渲染加入队列。在后续的重新渲染中,useState 返回的第一个值将始终是更新后最新的 state。不过,如果 setState 函数接受的值与当前 state 值完全相同,则随后的重渲染会被完全跳过。
注意,React 会确保 setState 函数的标识是稳定的,并且不会在组件重新渲染时发生变化。这就是为什么可以安全地从 useEffect 或 useCallback 的依赖列表中省略 setState。
函数式更新
如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值。实例:
1 | |
注意,与类组件的 this.setState 方法不同,useState 不会自动合并更新对象。可以用函数式的 setState 结合展开运算符来达到合并更新对象的效果。(useReducer 是另一种可选方案,更适合用于管理包含多个子值的 state 对象)
1 | |
惰性初始
initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并将返回的值作为初始 state,此函数只在初始渲染时被调用:
1 | |
跳过更新
如前所述,如果更新 State Hook 后的 state 与当前的 state 相同时,React 将跳过子组件的渲染并且不会触发相关 effect 的执行。(React 使用 Object.is 比较算法来比较 state)
需要注意的是,React 可能仍需要在跳过渲染前渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果在渲染期间执行了高开销的计算,则可以使用 useMemo 来进行优化。
合并更新
React 可能会将多次 state 更新合并到一次的重渲染中以改善性能。通常情况下,这能够提升性能并且不影响应用行为。
在 React 18 之前,只有 React 事件处理程序中的更新会被批处理。从 React 18 开始,默认情况下为所有更新启用批处理。注意,React 确保不同的“用户发起事件”(例如连续点击按钮)的更新始终各自独立处理,不会被批量处理。这可以避免逻辑上的错误。
在极少数情况下,您需要强制同步应用 DOM 更新,您可以将其包装在 flushSync 中。但是,这可能会影响性能,所以只在必要时使用这种方式。
useEffect
基础
1 | |
该 Hook 接收一个包含命令式、且可能有副作用代码的函数。
在函数组件主体内(指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。
使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。默认情况下,effect 将在每轮渲染结束后执行,但可以选择让它在只有某些值改变的时候才执行。
清除副作用
通常,组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源。要实现这一点,该函数需要返回一个清除函数。
1 | |
为了防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染(通常如此),则在执行下一个 effect 之前,上一个 effect 已被清除。在上述示例中,意味着组件的每一次更新都会创建新的订阅。若想避免每次更新都触发 effect 的执行,请参阅下一小节。
执行时机
与 componentDidMount、componentDidUpdate 不同的是,传给 useEffect 的函数在浏览器完成布局与绘制之后,在一个延迟事件中被调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因为绝大多数操作不应阻塞浏览器对屏幕的更新。
然而,并非所有的 effect 都可以被延迟执行。例如,一个对用户可见的 DOM 变更就必须在浏览器执行下一次绘制前被同步执行,这样用户才不会感觉到视觉上的不一致。(概念上类似于被动监听事件和主动监听事件的区别)React 为此提供了一个额外 useLayoutEffect Hook 来处理这类 effect。它和 useEffect 的结构相同,区别只是调用时机不同。
此外,从 React 18 开始,当它是离散的用户输入(如点击)的结果时,或当它是由 flushSync 包装的更新结果时,传递给 useEffect 的函数将在屏幕布局和绘制之前同步执行。这种行为便于事件系统或 flushSync 的调用者观察该效果的结果。
即使在 useEffect 被推迟到浏览器绘制之后的情况下,它也能保证在任何新的渲染前启动执行。React 在开始新的更新前,总会先刷新之前的渲染的 effect。
条件执行
默认情况下,effect 会在每轮组件渲染完成后执行。这样一旦 effect 的依赖发生变化,它就会被重新创建。然而,在某些场景下这么做可能会矫枉过正。比如,我们可能不需要在每次组件更新时都创建新的订阅,而是仅需要在一些值改变时重新创建。
要实现这一点可以给 useEffect 传递第二个参数,它是 effect 所依赖的值数组。依赖项数组不会作为参数传给 effect 函数。虽然从概念上来说它表现为所有 effect 函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。如:
1 | |
如果要使用此优化方式,需要确保数组中包含了所有外部作用域中会发生变化且在 effect 中使用的变量,否则代码会引用到先前渲染中的旧变量。
如果只想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 和 state 中的任何值,所以它永远都不需要重复执行。如果传入了一个空数组,effect 内部的 props 和 state 就会一直持有其初始值。尽管传入空数组作为第二个参数有点类似于 componentDidMount 和 componentWillUnmount 模式,但有更好的方式来避免过于频繁的重复调用 effect。除此之外,记得 React 会等待浏览器完成画面渲染之后才会延迟调用 useEffect,因此会使得处理额外操作很方便。注意不同:
- 依赖项为空:
effct会在每轮组件渲染完成后都执行 - 依赖项为空数组:
effct会在组件挂载时仅执行一次
useContext
基础
1 | |
接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。
当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。祖先用了 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。useContext(MyContext) 相当于类组件中的 static contextType = MyContext 或者 <MyContext.Consumer>。
注意,useContext 的参数必须是 context 对象本身,正确使用方法是 useContext(MyContext),而 useContext(MyContext.Consumer) 和 useContext(MyContext.Provider) 都是错误的。
调用了 useContext 的组件总会在 context 值变化时重新渲染。如果重渲染组件的开销较大,可以通过使用 memoization 来优化。实例:
1 | |
useReducer
基础
1 | |
这是 useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。
在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂并且包含多个子值,或者下一个 state 依赖于之前的 state 等等。并且使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为可以向子组件传递 dispatch 而不是回调函数。重写 useState 计数器实例:
1 | |
注意,React 会确保 dispatch 函数的标识是稳定的,并且不会在组件重新渲染时发生变化。这就是为什么可以安全地从 useEffect 或 useCallback 的依赖列表中省略 dispatch。
惰性初始化
可以选择惰性地创建初始 state。为此,需要将 init 函数作为 useReducer 的第三个参数传入,这样,初始的 state 将被设置为 init(initialArg)。
这么做可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利:
1 | |
跳过更新
如前所述,如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。(React 使用 Object.is 比较算法来比较 state)
需要注意的是,React 可能仍需要在跳过渲染前渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果在渲染期间执行了高开销的计算,则可以使用 useMemo 来进行优化。
useCallback
参考:https://juejin.cn/post/6844904101445124110
Demo:https://github.com/xuekeven/learn-react/blob/main/src/hook/useCallBack/index.tsx
基础
1 | |
接收内联回调函数及依赖项数组作为参数传入,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染的子组件时(例如 shouldComponentUpdate 的组件),它将非常有用。
依赖项数组不会作为参数传给回调函数。虽然从概念上来说它表现为:所有的回调函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。
使用
不要把所有的函数都包上 useCallback,其需要子组件配合一起使用 shouldComponentUpdate 或 React.memo 才能优化性能,否则就是反向优化。
组件重新渲染时,依赖项没有改变,useCallback 会从内部缓存中取出之前的函数直接返回到定义的变量。组件重新渲染时,依赖项发生改变,useCallback 会把传入的函数重新声明为一个新的函数,然后把新的函数返回到定义的变量。
如果子组件没有使用 shouldComponentUpdate 或 React.memo,即使没有重新声明新函数,返回的还是旧函数,子组件也还会重新渲染,这时使用 useCallback 消耗反而更大:每次执行到这里内部要比对依赖项是否变化,还要缓存一下之前的函数。
useMemo
参考:https://juejin.cn/post/6844904101445124110
Demo:https://github.com/xuekeven/learn-react/blob/main/src/hook/useMemo/index.tsx
基础
1 | |
接收创建函数和依赖项数组作为参数,它仅会在某个依赖项改变时才会执行创建函数并重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
useMemo 与 useCallback 很像,根据 useCallback 可以想到 useMemo 也能针对传入子组件的值进行缓存优化,有助于避免在每次渲染时都进行高开销的计算。记住,传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行不应该在渲染期间内执行的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。
如果没有提供依赖项数组,useMemo 会在每次渲染时都会计算新的值。依赖项数组不会作为参数传给创建函数。虽然从概念上来说它表现为:所有“创建”函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。
使用
可以把一些昂贵的计算逻辑放到 useMemo 中,只有当依赖值发生改变的时候才去更新:
1 | |
事实上,使用 useMemo 的场景远比 useCallback 广泛的多,可以将 useMemo 的返回值定义为返回一个函数,这样就可以变通地实现 useCallback。在开发中,当有部分变量改变会影响到多个地方的更新时,就可以返回一个对象或者数组,通过解构赋值的方式来实现同时对多个数据的缓存。
1 | |
对比
参考:https://juejin.cn/post/7104436526494253087
useCallback 与 useMemo 一个缓存的是函数,一个缓存的是执行完函数后函数的返回值。
useCallback 是来优化子组件的,防止子组件的重复渲染。useMemo 可以优化当前组件也可以优化子组件,优化当前组件主要是通过 memoize 来将一些复杂的计算逻辑进行缓存。如果只是进行简单的计算也没必要使用 useMemo,可以考虑一些计算的性能消耗和比较依赖项的性能消耗来做一个权衡。
关于是否使用这些,或许可以用一句话来总结:如果没有性能瓶颈,那就建议不用,大部分项目可能并不需要考虑以阻止 React 的渲染来提高性能 ——— 甚至可以说如果不能保证收获比成本大的“多”,那就尽量不用。这这句话也藏着性能优化的原则之一:不要过早优化。
useRef
基础
1 | |
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。
一个常见的用例便是命令式地访问子组件:
1 | |
本质上,useRef 就像是可以在其 .current 属性中保存一个可变值的“盒子”。
ref 是访问 DOM 的主要方式。如果将 ref 对象以 <div ref={myRef} /> 形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的 .current 属性设置为对应 DOM 节点。useRef() 比 ref 属性更有用,它可以很方便地保存任何可变值,其类似于在类组件中使用实例字段的方式,这是因为它创建的是一个普通 Javascript 对象。
useRef() 和自建一个 {current: ...} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象,而后者是每次新建对象。当 ref 对象内容发生变化时,useRef 并不会通知。变更 .current 属性不会引发组件的重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,需要使用回调 ref 来实现。
useImperativeHandle
基础
1 | |
useImperativeHandle 可以在使用 ref 时自定义暴露给父组件的实例值。大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用:
1 | |
useLayoutEffect
基础
其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。尽可能使用标准的 useEffect 以避免阻塞视觉更新。
注意,虽然 useLayoutEffect 与 componentDidMount、componentDidUpdate 的调用阶段一样。但还是推荐先用 useEffect,当出现问题的时候再尝试使用 useLayoutEffect。
执行时机
大多数组件不需要依靠它们在屏幕上的位置和大小来决定渲染什么。他们只返回一些 JSX,然后浏览器计算他们的 布局(位置和大小)并重新绘制屏幕。
有时候,这还不够。想象一下悬停时出现在某个元素旁边的 tooltip。如果有足够的空间,tooltip 应该出现在元素的上方,但是如果不合适,它应该出现在下面。为了让 tooltip 渲染在最终正确的位置,你需要知道它的高度(即它是否适合放在顶部)。要做到这一点,需要分两步渲染:
- 将 tooltip 渲染到任何地方(即使位置不对)
- 测量它的高度并决定放置 tooltip 的位置
- 把 tooltip 渲染放在正确的位置
所有这些都需要在浏览器重新绘制屏幕之前完成。不希望用户看到 tooltip 在移动,需要调用该 Hook 在浏览器重新绘制屏幕之前执行布局测量:
1 | |