
要解决的问题
计时器、订阅、事件监听的回调里经常要读最新的 props 或 state,但那些值一变,又不该把连接断开重来。
看官方那个计数器的例子:每秒把 count 加上当前的 increment。increment 会被用户改动,但它一变就重启计时器显然不对。
import { useState, useEffect, useEffectEvent } from 'react'
function Timer() { const [count, setCount] = useState(0) const [increment, setIncrement] = useState(1)
const onTick = useEffectEvent(() => { setCount(count + increment) })
useEffect(() => { const id = setInterval(() => onTick(), 1000) return () => clearInterval(id) }, [])
return ( <> <p>计数:{count}</p> <button onClick={() => setIncrement(i => i + 1)}>每次加 {increment}</button> </> )}onTick 读到的是最新的 count 与 increment,但它不进依赖数组,所以计时器只在挂载时建一次,读取的值却始终是新的。
它是什么
useEffectEvent(callback) 返回一个函数。它属于 Effect 的逻辑,但行为更像事件处理函数:调用时总能读到最近一次渲染提交的值。
19.2 起它转为稳定(React 19.2)。
四条规则
只能在 Effect 里调用。useEffect、useLayoutEffect、useInsertionEffect,或者同一组件里另一个 Effect Event 内部。渲染期间调用、或者把它当回调传给别的组件,都会报错,lint 也会拦。
不要用它来少写依赖。这条最要紧。如果某个值变化时 Effect 本来就该重跑,就把它写进依赖数组;把它藏进 Effect Event 只是把 bug 藏得更深。官方的原话是:这个 Hook 只给「确实不该触发 Effect 重跑」的逻辑用。
它的函数身份每次渲染都会变,这是有意设计。所以永远不要把它放进依赖数组:稳定身份反而会掩盖问题,而不稳定的身份会让写错的地方立刻表现为「Effect 每次都重跑」。
只能在组件或自定义 Hook 的顶层调用它。
自定义 Hook 里也能用
function useInterval(callback, delay) { const onTick = useEffectEvent(callback)
useEffect(() => { const id = setInterval(() => onTick(), delay) return () => clearInterval(id) }, [delay])}传进来的 callback 每次渲染都是新函数,但 onTick 不进依赖,计时器只在 delay 变化时重建。这是封装计时器、订阅类 Hook 的标准写法。
与 useCallback 的分工
useCallback 稳住引用,用在「这个函数要传出去」的场合:给子组件当 props、给别的 Hook 当依赖。
useEffectEvent 反过来,它刻意不稳定,只在本组件的 Effect 里用。两者不能互换:把 Effect Event 传给子组件会报错,用它去当依赖数组里的项也会被 lint 拦下。
与过去那个 ref 土办法对比
在它出现之前,常见做法是用 ref 存最新值,Effect 里读 ref.current:
const latest = useRef(count)useEffect(() => { latest.current = count})能用,但要多写一个 Effect、多一个 ref,还容易漏更新。useEffectEvent 把这件事收成一行。
分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 190 天
部分内容可能已过时