监听user变化拉取数据时,useCallback读写state引发的无限循环问题
问题根因
当前的触发死循环逻辑链路非常清晰:
- 你的
getData把loading作为useCallback的依赖项,只要loading状态变化,getData就会生成全新的函数引用 - 绑定了
[user, getData]依赖的useEffect,检测到getData引用变化就会重新执行,哪怕user根本没有发生变更 - 重新执行的
useEffect又会调用getData,进而修改loading状态,彻底形成无限触发的闭环
可落地方案
以下两个方案都能满足“其他场景下手动调用getData”的原有需求,不会破坏现有业务逻辑。
方案1:用ref存储loading状态,解耦getData对loading state的依赖
你在getData里写if (!loading)本质是实现防重复执行的控制逻辑,这个判断完全不需要直接读取state值——只要用配套的ref同步最新的loading状态,就能既保留防重能力,又不会把loading变成getData的useCallback依赖。
改法参考:
// 为loading配套同步状态的ref const loadingRef = useRef(false); const [loading, setLoading] = useState(false); const getData = useCallback(async () => { // 从ref读取loading状态,不直接引用state,不会触发useCallback依赖更新 if (loadingRef.current) return; try { // 进入请求后同步更新ref和state loadingRef.current = true; setLoading(true); const { error, data } = await getDataHook(); if (error) { throw new Error("blah!"); } // 业务处理逻辑写完后,记得重置状态 loadingRef.current = false; setLoading(false); } catch (error) { const message = getErrorMessage(error); setErrorMessage(message); // 异常分支也要重置状态 loadingRef.current = false; setLoading(false); } }, []); // 无多余依赖,getData的函数引用永远稳定 // 这时候useEffect只需要绑定稳定的依赖即可 useEffect(() => { getData(); }, [user, getData]);
这个方案的优势是:不管是user变化时的自动拉取,还是按钮点击、提交回调等场景的手动调用,getData的防重复执行逻辑都能正常生效,同时因为函数引用稳定,不会触发无意义的useEffect重复执行。
方案2:解耦纯拉取逻辑和防重控制逻辑
如果不想引入ref,可以把“拉取数据”和“防重复执行”的逻辑拆开,让自动触发的useEffect只引用稳定的纯拉取函数,手动调用的入口保留带防重能力的逻辑。
改法参考:
// 纯拉取逻辑,无多余依赖,引用永远稳定 const fetchData = useCallback(async () => { const { error, data } = await getDataHook(); if (error) throw new Error("blah!"); // 处理返回数据的业务逻辑 }, []); // 对外暴露的getData入口,只负责包裹防重逻辑,供其他场景手动调用 const getData = useCallback(async () => { if (loading) return; setLoading(true); try { await fetchData(); } catch (error) { const message = getErrorMessage(error); setErrorMessage(message); } finally { // 不管成功失败都重置loading setLoading(false); } }, [loading, fetchData]); // user变化时的自动拉取,直接调用稳定的fetchData,不经过带依赖的getData useEffect(() => { fetchData(); }, [user, fetchData]);
这个方案更适合业务场景拆分:自动拉取(比如user切换)本来就不需要被当前的loading状态拦截,而手动调用(比如刷新按钮)需要防重复点击,两边逻辑互不干扰。
注意:不要为了规避这个问题直接用
// eslint-disable-next-line关掉useEffect的依赖检查,后续代码迭代时很容易产生闭包读取旧值的隐蔽bug。
内容的提问来源于stack exchange,提问作者brendangibson
相关产品推荐
相关产品推荐

