React中useCallback结合resize监听时state不更新问题排查
我编写了一个useDeviceDetect钩子,可正常根据屏幕宽度判断当前设备是桌面端、平板还是移动端。现在在组件中需要仅在移动端计算某个元素的高度,同时监听window的resize事件以应对文本换行导致的高度变化,但组件内的handleWindowSizeChange函数里,isDesktop始终保持初始值,即便使用了useCallback钩子也无法解决。
组件代码如下:
const handleWindowSizeChange = useCallback(() => { console.log(isDesktop) // 若初始为桌面端则始终为true,初始为移动端则始终为false }, [isDesktop]); useEffect(() => { console.log(isDesktop) // 触发断点时会输出正确值 }, [isDesktop]); useEffect(() => { if (!loaded) return; handleWindowSizeChange(); window.addEventListener('resize', handleWindowSizeChange); return () => { window.removeEventListener('resize', handleWindowSizeChange); } }, [loaded]);
设备检测钩子代码如下:
'use client' import { useState, useEffect } from 'react'; import { throttle } from './utils'; export default function useDeviceDetect() { const [isMobile, setMobile] = useState(false); const [isTablet, setTablet] = useState(false); const [isDesktop, setDesktop] = useState(false); const [loaded, setLoaded] = useState(false); const breakpointSmall = 480; const breakpointMedium = 960; const breakpointLarge = 1280; const breakpointXLarge = 1680; function handleWindowSizeChange() { if (window.screen.width < breakpointSmall) { setMobile(true); setTablet(false); setDesktop(false); } if (window.screen.width >= breakpointSmall && window.screen.width < breakpointMedium) { setMobile(false); setTablet(true); setDesktop(false); } if (window.screen.width >= breakpointMedium && window.screen.width < breakpointLarge) { setMobile(false); setTablet(false); setDesktop(true); } if (window.screen.width >= breakpointLarge && window.screen.width < breakpointXLarge) { setMobile(false); setTablet(false); setDesktop(true); } if (window.screen.width >= breakpointXLarge) { setMobile(false); setTablet(false); setDesktop(true); } setLoaded(true); } useEffect(() => { handleWindowSizeChange(); window.addEventListener('resize', handleWindowSizeChange); return () => { window.removeEventListener('resize', handleWindowSizeChange); } }, []); return { isMobile, isTablet, isDesktop, loaded }; }
我理解useCallback会缓存函数,并在依赖项变化时重新创建,原以为该钩子可以解决类似setInterval中state不更新的问题。请问我是代码实现有误,还是误解了useCallback的用途?
问题原因
你对useCallback的理解没错,它确实会在依赖变化时重新生成函数实例,但问题出在resize事件的监听没有跟随函数更新重新绑定。
你只在loaded变化时执行了一次事件绑定(也就是组件初始加载时),后续isDesktop更新导致handleWindowSizeChange重新创建后,新的函数并没有被绑定到resize事件上,window上始终挂着第一次的旧函数——而旧函数的闭包中捕获的是初始的isDesktop值,所以无论后续状态怎么变,旧函数输出的都是初始值。
修复方案
方案一:更新事件监听的依赖项,绑定最新的函数实例
修改组件中负责绑定resize事件的useEffect,把handleWindowSizeChange加入依赖数组,这样每次函数更新时,都会移除旧的监听,绑定新的函数:
useEffect(() => { if (!loaded) return; handleWindowSizeChange(); window.addEventListener('resize', handleWindowSizeChange); return () => { window.removeEventListener('resize', handleWindowSizeChange); } }, [loaded, handleWindowSizeChange]); // 新增handleWindowSizeChange作为依赖
方案二:简化逻辑,直接依赖设备状态变化执行操作
既然useDeviceDetect已经在监听resize并更新设备状态,你可以直接依赖isMobile的变化来触发高度计算,不需要额外绑定resize事件:
// 移除单独的resize监听,直接在设备状态变化时执行逻辑 useEffect(() => { if (!loaded) return; if (!isMobile) return; // 仅在移动端执行 // 这里写入计算元素高度的逻辑 }, [loaded, isMobile]);
如果必须监听resize(比如文本换行导致高度变化,但设备类型未改变),可以在handleWindowSizeChange中直接通过window.screen.width判断设备类型,或者确保事件绑定的是最新的函数实例(方案一)。
内容的提问来源于stack exchange,提问作者MPN7

