React组件卸载时useEffect清理函数中state非最新,如何获取最新值?
问题原因
你遇到的是典型的闭包陷阱:你当前写的useEffect依赖项是空数组,所以这个effect仅会在组件首次挂载时执行1次,它返回的清理函数捕获的是首次挂载时的上下文,里面的state都是初始值,后续state更新不会同步到这个闭包里,所以拿到的永远是初始值。
解决方法
方法1:将需要用到的state加入useEffect依赖项
这是最直接的解法,把清理函数里用到的state加到依赖数组里,每次state更新时effect都会重新执行,清理函数也会同步捕获最新的state值。
注意:这个方案下,清理函数不仅会在组件卸载时执行,还会在每次依赖更新前执行。如果你只需要在卸载时执行业务逻辑,可以加个标记判断:
const [num, setNum] = useState(4); const isUnmounting = useRef(false); // 专门标记是否进入卸载流程 useEffect(() => { return () => { isUnmounting.current = true; }; }, []); useEffect(() => { return () => { if (isUnmounting.current) { // 只有组件真正卸载时才执行这里的逻辑 console.log("卸载时的最新num值:", num); // 在这里发起你的HTTP请求即可 } }; }, [num]); // 把需要用到的state加到依赖里
方法2:用useRef同步最新state
如果你不想让effect依赖state、也不想频繁触发effect的执行,可以用useRef来存储最新的state值,每次state更新时同步给ref,清理函数直接读取ref的current值即可——ref的引用不会变,current属性永远存储的是最新值:
const [num, setNum] = useState(4); const numRef = useRef(num); // 实时同步state到ref useEffect(() => { numRef.current = num; }, [num]); useEffect(() => { return () => { // 直接读ref就能拿到最新值 console.log("卸载时的最新num值:", numRef.current); // 在这里发起你的HTTP请求即可 }; }, []); // 原effect的依赖还是空数组,仅挂载时执行一次
这个方案的好处是侵入性低,不需要修改原有空依赖effect的执行逻辑,清理函数只会在卸载时执行一次。
方法3:使用React 18的useEffectEvent(推荐)
React 18新增了useEffectEventAPI(目前已稳定),专门用来解决这类问题:你可以把需要读取最新state、但不需要作为effect依赖的逻辑放到useEffectEvent包裹的函数里,这个函数不需要加到effect的依赖中,内部永远能拿到最新的state和props:
import { useEffect, useEffectEvent, useState } from 'react'; const [num, setNum] = useState(4); // 用useEffectEvent包裹卸载逻辑 const handleUnmount = useEffectEvent(() => { // 这里永远能拿到最新的num,不需要加到依赖里 console.log("卸载时的最新num值:", num); // 在这里发起你的HTTP请求即可 }); useEffect(() => { return () => { handleUnmount(); }; }, []); // 依赖还是空数组,只会挂载执行一次
额外兼容场景:页面关闭/刷新
如果你的需求是用户离开页面(包括关闭标签页、刷新页面)时发请求,仅靠React组件卸载的清理函数是不够的,这种场景下页面会直接终止进程,React的清理逻辑可能来不及执行,需要额外监听beforeunload事件,并且用navigator.sendBeacon来发请求,浏览器会保证这个请求在页面关闭前完成:
useEffect(() => { const handleBeforeUnload = () => { // 用sendBeacon发请求不会被浏览器中断 navigator.sendBeacon('你的接口地址', JSON.stringify({ num: numRef.current })); }; window.addEventListener('beforeunload', handleBeforeUnload); return () => { window.removeEventListener('beforeunload', handleBeforeUnload); // 组件正常卸载时的逻辑 handleUnmount(); }; }, []);
内容的提问来源于stack exchange,提问作者tilly

