useCallback 没拦住重渲染:闭包里的旧值才是真凶
【摘要】 一个轮询组件的bug让我重新理解了useCallback。现象:定时器里每5秒上报一次数据,但上报的永远是「进入页面那一刻」的旧配置,改了刷新间隔和筛选条件都不生效。最小化后的出事代码:functionPollPanel({interval,params}:Props){const[count,setCount]=useState(0)constreport=useCallback(()=&...
一个轮询组件的 bug 让我重新理解了 useCallback。现象:定时器里每 5 秒上报一次数据,但上报的永远是「进入页面那一刻」的旧配置,改了刷新间隔和筛选条件都不生效。
最小化后的出事代码:
function PollPanel({ interval, params }: Props) {
const [count, setCount] = useState(0)
const report = useCallback(() => {
// 依赖数组为空 → report 永远捕获初次渲染时的 props
fetch('/api/report', {
method: 'POST',
body: JSON.stringify({ count, interval, params })
})
}, []) // 这里是问题
useEffect(() => {
const timer = setInterval(report, interval)
return () => clearInterval(timer)
}, [report]) // interval 变了,但 report 没变,定时器也不会重设
}
两个坑叠加了。第一个坑:依赖数组为空,JS 的闭包特性决定了 report 只能看见第一次渲染时的 count 和 props,无论外面怎么变,函数体内都是「过去」。第二个坑:useEffect 依赖了 report,而空依赖的 useCallback 返回同一个函数引用,effect 永远不会重建,interval 更新自然无效。
修复的版本:
const report = useCallback(() => {
fetch('/api/report', {
method: 'POST',
body: JSON.stringify({ count, interval, params })
})
}, [count, interval, params]) // 依赖写全,闭包就是新的
useEffect(() => {
const timer = setInterval(report, interval)
return () => clearInterval(timer) // report/interval 变化时清理重建
}, [report, interval])
依赖写全之后,「配置变了」会反映在闭包里,effect 也会按新的 interval 重建定时器。
如果觉得「report 每次 count 变都重建」浪费,更优雅的路线是 useRef 存最新函数,定时器只建一次:
const latest = useRef(report)
useEffect(() => { latest.current = report }) // 每次渲染后更新引用
useEffect(() => {
const timer = setInterval(() => latest.current(), interval)
return () => clearInterval(timer)
}, [interval]) // 定时器只依赖 interval
这轮排查沉淀了两个习惯:一是装 react-hooks/exhaustive-deps 这个 ESLint 规则,空依赖数组会给出警告,这次事故的直接原因就是这条规则被项目关掉了;二是记住 useCallback 的定位——它解决「引用稳定性」,不解决「数据新鲜度」,闭包旧值问题只能靠依赖完整性或 ref 模式解决。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)