React Context值在useCallback包裹的动画函数内不更新的问题求助
React Context值在useCallback包裹的动画函数内不更新的问题求助
嗨,我最近遇到了一个React Context相关的问题,折腾了好久没搞定,想请教大家帮忙分析下~
我的需求是:调用一个循环动画的同时执行一个异步函数,等异步函数执行完成后停止动画。但现在出现了一个奇怪的问题:在组件主体里打印Context值是正常更新的,但在useCallback包裹的animate动画函数里,Context值始终停留在初始状态,完全不更新。我已经把Context加到useCallback的依赖数组里了,可还是没效果,这到底是为什么呀?
我的代码片段如下:
console.log(context, "context value outside") // 这里能正常更新 const animate = useCallback( async (index = 0) => { console.log(context, "context value inside") // 这里完全不更新 return setTimeout(() => { // 执行动画逻辑 if (index === 3) { animate(0) } else { animate(index + 1) } }, 800) }, [context] )
当我调用触发函数同时执行animate()和getAsyncFunction()时,就会出现上面的问题,明明组件里的Context已经变了,动画函数里就是拿不到最新值。
问题原因分析
其实这是典型的闭包陷阱导致的:
- 当你第一次调用
animate时,这个函数会捕获当前时刻的context值; - 之后即使
context更新,触发useCallback生成了新的animate函数,但你之前启动的动画循环里,setTimeout的回调引用的还是第一次调用的旧animate函数; - 旧的
animate函数里的context是它被创建时捕获的初始值,所以不管外部context怎么变,这里始终拿不到最新值。
解决方案
我们可以通过useRef来规避闭包陷阱,同时添加动画停止的控制逻辑:
1. 用useRef保存最新的Context值
// 用ref实时保存最新的context const contextRef = useRef(context); useEffect(() => { contextRef.current = context; }, [context]); const animate = useCallback( async (index = 0) => { console.log(contextRef.current, "context value inside"); // 现在能拿到最新值了 return setTimeout(() => { // 执行动画逻辑 if (index === 3) { animate(0) } else { animate(index + 1) } }, 800) }, [] // 这里不需要再依赖context了 )
2. 添加动画停止的控制逻辑
因为你的动画是递归调用的,单纯清除单个timeout可能不够,建议用一个标志位来控制动画是否继续:
const contextRef = useRef(context); const isAnimating = useRef(true); // 控制动画是否继续的标志 const timeoutRef = useRef(null); // 保存timeoutID,方便清除 useEffect(() => { contextRef.current = context; }, [context]); const animate = useCallback( async (index = 0) => { // 如果已经停止动画,直接返回 if (!isAnimating.current) return; console.log(contextRef.current, "context value inside"); timeoutRef.current = setTimeout(() => { // 执行动画逻辑 if (index === 3) { animate(0) } else { animate(index + 1) } }, 800) }, [] ) // 你的异步函数,完成后停止动画 const getAsyncFunction = async () => { await yourAsyncTask(); // 替换成你的异步逻辑 // 停止动画 isAnimating.current = false; if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } }
这样修改后,动画函数里就能拿到最新的Context值,同时异步函数完成后也能正确停止动画啦~
备注:内容来源于stack exchange,提问作者Red Baron
相关产品推荐
相关产品推荐

