React Hook中ESLint提示赋值丢失,是否应忽略该警告?
问题背景
你编写了如下自定义React Hook:
type Callback = () => void function useElementEffect<T extends Element>(callback: (el: T) => (Callback | void)) { let cb: Callback | void return useCallback((el: T) => { if(el) { cb = callback(el) // <-- eslint error here } else if(cb) { cb() } }, []) }
使用示例:
function CenterCanvas() { const [[width, height], setSize] = useState([300, 150]) const ref = useElementEffect((div: HTMLDivElement) => { const obs = new ResizeObserver(entries => { for(const entry of entries) { const contentBoxSize = entry.contentBoxSize[0] setSize([ Math.floor(contentBoxSize.inlineSize), Math.floor(contentBoxSize.blockSize) ]) } }) obs.observe(div) return () => obs.disconnect() }) return ( <$Center ref={ref}> <TheCanvas width={width} height={height}/> </$Center> ) }
该Hook运行正常,但cb = callback(el)行触发ESLint警告:
ESLint: Assignments to the 'cb' variable from inside React Hook useCallback will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useCallback.(react-hooks/exhaustive-deps)
你发现组件卸载时cb()能正常执行,想知道是否该忽略警告,或存在潜在失效场景。
为什么当前能正常运行?
当前代码能正常工作的核心原因是:useCallback依赖空数组,返回的ref回调函数是稳定的闭包——组件每次渲染时,这个函数的引用都不会变,它捕获的是第一次调用useElementEffect时声明的cb变量。只要这个闭包存在(组件未卸载),cb的值就会被保留,所以组件卸载时React调用ref回调传入null,能正常触发之前保存的清理函数。
潜在失效场景
虽然当前场景没问题,但存在以下隐患:
callback更新失效:如果useElementEffect的callback参数在组件后续渲染中发生变化(比如示例中CenterCanvas的内联回调会在每次渲染时重新创建),但useCallback的依赖数组为空,不会更新ref回调函数,导致后续元素挂载时仍然调用旧的callback,新的逻辑无法生效,旧的清理函数也可能无法被正确替换。- 闭包变量混淆:每次组件渲染时,
useElementEffect都会重新声明一个新的cb变量,但ref回调函数始终引用第一次渲染的cb。如果后续渲染中存在其他操作意外修改这个变量,可能导致难以排查的逻辑错误。 - 不符合React Hooks规范:直接在Hook作用域中声明可变变量并在闭包中修改,不属于React推荐的状态管理方式,容易引发闭包陷阱,降低代码可维护性。
正确的处理方式
按照ESLint提示,用useRef保存清理函数,这是React Hooks中保存跨渲染可变值的标准做法:
type Callback = () => void function useElementEffect<T extends Element>(callback: (el: T) => (Callback | void)) { const cbRef = useRef<Callback | void>() return useCallback((el: T) => { if(el) { cbRef.current = callback(el) } else if(cbRef.current) { cbRef.current() cbRef.current = undefined // 清理引用,避免内存泄漏 } }, [callback]) // 加入callback依赖,确保回调更新时能获取最新逻辑 }
改进点说明:
- 用
useRef的.current属性存储清理函数,它在组件整个生命周期中是同一个引用,不受渲染影响。 - 将
callback加入useCallback的依赖数组,确保callback更新时,ref回调函数能同步更新,调用最新的逻辑。 - 执行清理后重置
cbRef.current,避免不必要的内存占用。
结论
不要忽略ESLint警告,用useRef重构代码能消除潜在的失效场景,同时符合React Hooks的最佳实践,提升代码的稳定性和可维护性。
内容的提问来源于stack exchange,提问作者mpen

