React Context(Hooks)中Modal的onClose回调未更新为最新值问题
问题根因
你遇到的问题本质是闭包陷阱,核心诱因如下:
useClickOutside钩子的useEffect依赖数组不完整,仅声明了ref,漏掉了传入的onClickOutside参数。钩子初始化时绑定的事件监听永远引用第一次传入的closeModal实例,此时onClose还是初始值null,后续onClose更新后事件监听也不会同步获取最新的closeModal。- 按钮点击能正常执行是因为按钮的
onClick每次渲染都会读取最新的closeModal实例,天然能拿到最新的onClose回调。 - 额外隐性问题:
useClickOutside中监听事件绑定在document上,销毁时却从window上移除,会导致事件监听泄漏。
修复方案
方案1:补全依赖+优化函数引用
第一步:修复useClickOutside依赖与事件移除逻辑
import { useRef, useEffect } from 'react'; function useClickOutside(ref, onClickOutside: () => void) { const handleClickOutside = (event) => { if (ref.current && !ref.current.contains(event.target)) { onClickOutside(); } }; useEffect(() => { document.addEventListener("mousedown", handleClickOutside); return () => { // 修正移除事件的目标对象,和监听对象保持一致 document.removeEventListener("mousedown", handleClickOutside); }; // 补全onClickOutside依赖 }, [ref, onClickOutside]); }
第二步:用useCallback包裹closeModal减少不必要的实例更新
import { useState, useCallback } from 'react'; function ModalProvider({ children }) { const [component, setComponent] = useState<React.ReactNode>(null); const [styles, setStyles] = useState<React.CSSProperties>(null); const [onClose, setOnClose] = useState<() => void>(null); const openModal = ({ component, containerStyles, onClose, }: OpenModalProps) => { setComponent(component); setStyles(containerStyles); setOnClose(() => onClose); }; // 用useCallback包裹,仅当onClose更新时才生成新的closeModal实例 const closeModal = useCallback(() => { onClose?.(); setComponent(null); setStyles(null); setOnClose(null); }, [onClose]); return ( <ModalContext.Provider value={{ component, styles, openModal, closeModal }}> {children} </ModalContext.Provider> ); }
方案2:更稳定的回调缓存方案(推荐)
如果想要避免事件频繁解绑重绑,可以在useClickOutside中用ref缓存最新的回调,不需要修改依赖数组:
import { useRef, useEffect } from 'react'; function useClickOutside(ref, onClickOutside: () => void) { // 用ref存储最新的回调引用 const onClickOutsideRef = useRef(onClickOutside); // 回调更新时同步刷新ref useEffect(() => { onClickOutsideRef.current = onClickOutside; }, [onClickOutside]); const handleClickOutside = (event) => { if (ref.current && !ref.current.contains(event.target)) { // 始终调用最新的回调 onClickOutsideRef.current(); } }; useEffect(() => { document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, [ref]); }
内容的提问来源于stack exchange,提问作者Miguel Rodriguez
相关产品推荐
相关产品推荐

