React Hook中函数内state意外缓存问题咨询
我现在用的是很常见的自定义Hook模式:内部维护state,同时返回操作该state的方法。但遇到一个问题:updateAndGetPolygon函数里的loadedPolys有时候会被缓存,导致函数内外的console.log输出值不一样。就算用useCallback包裹这个函数也解决不了,只有不用useState才生效。相关代码如下:
export function useLoadedViewports() { const [loadedPolys, setLoadedPolys] = useState([]); console.log('loadedPolys outside:', loadedPolys); const updateAndGetPolygon = (poly) => { console.log('loadedPolys inside:', loadedPolys); // Some logic setting updated state const differenceResult = difference(poly, loadedPolys); const unionResult = union(poly, loadedPolys); setLoadedPolys(unionResult); return differenceResult; }; return { updateAndGetPolygon, }; }
问题根源:闭包陷阱
React组件(包括自定义Hook)每次渲染时,都会重新执行一遍内部代码,生成新的函数和变量。updateAndGetPolygon作为渲染时创建的函数,会捕获当前渲染周期里的loadedPolys值。
如果这个函数被外部缓存(比如父组件用useMemo/useCallback没处理好依赖,或者传递给子组件后子组件做了缓存),它就会一直持有旧渲染周期里的loadedPolys引用,而外部的console.log打印的是当前最新渲染的loadedPolys,自然两者输出不一致。
为什么useCallback没用?
如果你只是用useCallback包裹updateAndGetPolygon但没把loadedPolys加入依赖数组,那这个函数会被一直缓存,捕获的永远是第一次渲染的loadedPolys;如果加了loadedPolys到依赖,每次loadedPolys变化函数都会更新,这时候就不会有缓存问题,但可能会导致依赖这个函数的组件频繁重渲染。
解决方法
1. 使用函数式更新获取最新state
React的setState支持传入函数,函数的参数是当前最新的state值,这样就不用依赖闭包里的loadedPolys了:
export function useLoadedViewports() { const [loadedPolys, setLoadedPolys] = useState([]); console.log('loadedPolys outside:', loadedPolys); const updateAndGetPolygon = (poly) => { let differenceResult; // 用函数式更新获取最新的prevLoadedPolys setLoadedPolys(prevLoadedPolys => { differenceResult = difference(poly, prevLoadedPolys); const unionResult = union(poly, prevLoadedPolys); return unionResult; }); return differenceResult; }; return { updateAndGetPolygon, }; }
这种方式能确保你用到的是最新的state,同时避免闭包带来的缓存问题。
2. 用useRef保存state的实时引用
如果需要在函数中直接获取最新的loadedPolys,可以用useRef同步state的值:
export function useLoadedViewports() { const [loadedPolys, setLoadedPolys] = useState([]); const loadedPolysRef = useRef(loadedPolys); // 每次state更新时同步ref useEffect(() => { loadedPolysRef.current = loadedPolys; }, [loadedPolys]); console.log('loadedPolys outside:', loadedPolys); const updateAndGetPolygon = (poly) => { // 直接用ref.current获取最新值 console.log('loadedPolys inside:', loadedPolysRef.current); const differenceResult = difference(poly, loadedPolysRef.current); const unionResult = union(poly, loadedPolysRef.current); setLoadedPolys(unionResult); return differenceResult; }; return { updateAndGetPolygon, }; }
3. 正确配置useCallback的依赖
如果你必须用useCallback,一定要把loadedPolys加入依赖数组,确保函数每次state变化时更新:
export function useLoadedViewports() { const [loadedPolys, setLoadedPolys] = useState([]); console.log('loadedPolys outside:', loadedPolys); const updateAndGetPolygon = useCallback((poly) => { console.log('loadedPolys inside:', loadedPolys); const differenceResult = difference(poly, loadedPolys); const unionResult = union(poly, loadedPolys); setLoadedPolys(unionResult); return differenceResult; }, [loadedPolys]); // 加入loadedPolys作为依赖 return { updateAndGetPolygon, }; }
这种方式会让函数在loadedPolys变化时重新生成,确保捕获的是最新的state值,但要注意依赖这个函数的组件可能会因此频繁重渲染,必要时可以配合React.memo优化。
内容的提问来源于stack exchange,提问作者wra

