React useEffect绑定事件中state不更新问题排查咨询
问题描述
我有两个React组件:LinkItem与DropdownMenu。点击LinkItem内的按钮可切换DropdownMenu的显示/隐藏,点击菜单外部需触发onClickOutside回调隐藏菜单。但DropdownMenu组件中,渲染阶段的console.log('DropdownMenu menuVisible2: '+menuVisible)能输出正确的state值(菜单也能正常显隐),而useEffect绑定的handleClickOutside函数内的console.log('DropdownMenu menuVisible1: '+menuVisible)输出的state值却不正确,请问该现象的原因是什么?
组件代码:LinkItem
export default function LinkItem({ url, desc }) { const [menuVisible, setMenuVisible] = useState(false); const [menuPosition, setMenuPosition] = useState({ left: 0, top: 0 }); return ( <> <a className="bru-link-link" href={url} target="_blank"> <div className="bru-link-data"> <div className="bru-link-linkurl">{url}</div> <div className="bru-link-linkdesc">{desc}</div> </div> <div className="bru-link-menu" data-dropdown-toggle="dropdown"> <ImageButton onClick={(event) => { event.preventDefault(); setMenuVisible(!menuVisible); console.log("LinkItem setMenuVisible " + menuVisible); setMenuPosition({ left: event.nativeEvent.pageX, top: event.nativeEvent.pageY, }); }} > <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="3" stroke="currentColor" className="w-6 h-6" > <path strokeLinecap="round" strokeLinejoin="round" d="M12 6.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 12.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 18.75a.75.75 0 110-1.5.75.75 0 010 1.5z" /> </svg> </ImageButton> </div> </a> <DropdownMenu menuVisible={menuVisible} posTop={menuPosition.top} posLeft={menuPosition.left} onClickOutside={() => { console.log("LinkItem handleMenuClose"); setMenuVisible(false); }} /> </> ); }
组件代码:DropdownMenu
export default function DropdownMenu({ menuVisible, posLeft, posTop, onClickOutside, }) { const wrapperRef = useRef(null); const handleClickOutside = (event) => { //console.log(event.target); //console.log(wrapperRef.current); console.log("DropdownMenu menuVisible1: "+menuVisible); if (wrapperRef.current && !wrapperRef.current.contains(event.target)) { console.log("DropdownMenu onClickOutside"); onClickOutside(); } }; useEffect(() => { document.addEventListener("click", handleClickOutside, false); return () => { document.removeEventListener("click", handleClickOutside, false); }; }, []); console.log("DropdownMenu menuVisible2: "+menuVisible); return ( menuVisible && (<> <ul ref={wrapperRef} className={` absolute z-[1000] float-left m-0 min-w-max list-none overflow-hidden rounded-lg border-none bg-white bg-clip-padding text-left text-base shadow-lg dark:bg-neutral-700 [&[data-te-dropdown-show]]:block `} aria-labelledby="dropdownMenuButton1" data-te-dropdown-menu-ref style={{ left: posLeft + "px", top: posTop + "px", position: "absolute", }} > <li className="block w-full whitespace-nowrap bg-transparent py-2 px-4 text-sm font-normal text-neutral-700 hover:bg-neutral-100 active:text-neutral-800 active:no-underline disabled:pointer-events-none disabled:bg-transparent disabled:text-neutral-400 dark:text-neutral-200 dark:hover:bg-neutral-600"> Action </li> <li className="block w-full whitespace-nowrap bg-transparent py-2 px-4 text-sm font-normal text-neutral-700 hover:bg-neutral-100 active:text-neutral-800 active:no-underline disabled:pointer-events-none disabled:bg-transparent disabled:text-neutral-400 dark:text-neutral-200 dark:hover:bg-neutral-600"> Another action </li> <li className="block w-full whitespace-nowrap bg-transparent py-2 px-4 text-sm font-normal text-neutral-700 hover:bg-neutral-100 active:text-neutral-800 active:no-underline disabled:pointer-events-none disabled:bg-transparent disabled:text-neutral-400 dark:text-neutral-200 dark:hover:bg-neutral-600"> Something else here </li> </ul> </>) ); }
问题原因
这是React闭包陷阱导致的:
useEffect的依赖数组是空的[],意味着这个effect只会在组件挂载时执行一次,绑定的handleClickOutside是组件第一次渲染时创建的版本。- 这个初始版本的
handleClickOutside会捕获当时的menuVisible值(也就是初始的false),之后即使父组件传递的menuVisibleprops更新,这个函数里的menuVisible依然是第一次捕获的旧值,所以控制台输出永远是初始值。 - 而渲染阶段的
console.log('DropdownMenu menuVisible2: '+menuVisible)是在每次组件重新渲染时执行的,每次都会拿到最新的menuVisibleprops,所以输出正确。
解决方案
可以通过以下几种方式修复:
方案1:添加依赖到useEffect
把menuVisible和onClickOutside加入useEffect的依赖数组,这样每次这两个值变化时,都会重新绑定最新的handleClickOutside函数:
useEffect(() => { document.addEventListener("click", handleClickOutside, false); return () => { document.removeEventListener("click", handleClickOutside, false); }; }, [menuVisible, onClickOutside]); // 添加依赖
方案2:用useRef保存最新的props值
如果不想频繁解绑/绑定事件,可以用useRef实时更新保存最新的menuVisible值,在handleClickOutside里取ref的当前值:
export default function DropdownMenu({ menuVisible, posLeft, posTop, onClickOutside, }) { const wrapperRef = useRef(null); const menuVisibleRef = useRef(menuVisible); // 实时更新ref的值 useEffect(() => { menuVisibleRef.current = menuVisible; }, [menuVisible]); const handleClickOutside = (event) => { // 取ref的当前值 console.log("DropdownMenu menuVisible1: "+menuVisibleRef.current); if (wrapperRef.current && !wrapperRef.current.contains(event.target) && menuVisibleRef.current) { console.log("DropdownMenu onClickOutside"); onClickOutside(); } }; useEffect(() => { document.addEventListener("click", handleClickOutside, false); return () => { document.removeEventListener("click", handleClickOutside, false); }; }, [onClickOutside]); console.log("DropdownMenu menuVisible2: "+menuVisible); // 其余代码不变... }
方案3:用useCallback包裹handleClickOutside
配合useEffect的依赖,确保handleClickOutside只在依赖变化时重新创建:
const handleClickOutside = useCallback((event) => { console.log("DropdownMenu menuVisible1: "+menuVisible); if (wrapperRef.current && !wrapperRef.current.contains(event.target)) { console.log("DropdownMenu onClickOutside"); onClickOutside(); } }, [menuVisible, onClickOutside]); // 添加依赖 useEffect(() => { document.addEventListener("click", handleClickOutside, false); return () => { document.removeEventListener("click", handleClickOutside, false); }; }, [handleClickOutside]); // 依赖handleClickOutside
内容的提问来源于stack exchange,提问作者phonebuilder
相关产品推荐
相关产品推荐

