React侧边导航点击汉堡按钮异常及重渲染优化问题
我来帮你拆解下这个问题的解决思路:首先你遇到的侧边栏点击汉堡图标无法关闭的问题,核心是两个状态更新逻辑的时序冲突——useClickedOutside钩子先检测到点击在侧边栏组件外部(当时汉堡在侧边栏外面),把侧边栏状态设为false;紧接着汉堡的点击切换函数发现当前状态是false,又立刻把它设回true,导致侧边栏刚关上就重新打开了。
你把汉堡组件移到侧边栏内部的思路非常正确,这样点击汉堡时会被侧边栏的ref识别为内部点击,不会触发外部点击关闭的逻辑,冲突就解决了。但随之而来的侧边栏props变化时汉堡组件全量重渲染的问题,我们可以用React.memo配合useCallback来优化。
用React.memo+useCallback阻止不必要的重渲染
React.memo是React提供的高阶组件,它会对组件的props做浅比较,只有当props真正发生变化时才会触发组件重渲染。针对你EDIT后的代码,我们可以这样改造:
步骤1:将SideToggle组件移到SideNav外部
不要把SideToggle定义在SideNav组件内部,不然每次SideNav渲染时都会重新创建这个组件的新实例,React.memo就起不到缓存作用了。
步骤2:用React.memo包裹SideToggle,并稳定props引用
给SideToggle传入它实际需要的props,同时用useCallback确保点击回调的引用稳定,避免不必要的重渲染触发。
改造后的完整代码示例:
// SideNav.jsx // 把SideToggle提取到外部,并用React.memo包裹 const SideToggle = React.memo(({ sideIsOpen, handleToggle }) => { return ( <Toggle onClick={handleToggle}> <Bars> <Bar sideIsOpen={sideIsOpen} /> <Bar sideIsOpen={sideIsOpen} /> <Bar sideIsOpen={sideIsOpen} /> </Bars> </Toggle> ); }); export default function SideNav() { const { sideIsOpen, sideNavClose, sideNavOpen } = useContext(NavigationContext); // 用useCallback缓存点击回调,确保引用稳定 const handleToggle = useCallback(() => { sideIsOpen ? sideNavClose() : sideNavOpen(); }, [sideIsOpen, sideNavOpen, sideNavClose]); const handlers = useSwipeable({ onSwipedRight: () => { sideNavClose(); }, preventDefaultTouchmoveEvent: true, trackMouse: true, }); const sideRef = React.createRef(); useClickedOutside(sideRef, (callback) => { if (callback) { sideNavClose(); } }); return ( <> <SideToggle sideIsOpen={sideIsOpen} handleToggle={handleToggle} /> <div ref={sideRef}> <Side sideIsOpen={sideIsOpen} {...handlers}> <Section> <Container> <Col xs={12}>{/* <Menu /> */}</Col> </Container> </Section> </Side> </div> </> ); }
关键细节解释
- React.memo的作用:它会对比
SideToggle前后两次接收的sideIsOpen和handleToggle,只有当这两个值的引用发生变化时,才会重新渲染组件。 - useCallback的必要性:如果不用
useCallback包裹handleToggle,每次SideNav渲染时都会生成一个新的函数引用,React.memo会误以为props变化了,导致SideToggle依旧会重渲染。useCallback会把函数缓存起来,只有当依赖数组里的sideIsOpen、sideNavOpen、sideNavClose变化时,才会生成新的函数。
补充:最初冲突问题的另一种备选解决方案
如果你不想把汉堡移到侧边栏内部,也可以修改useClickedOutside钩子,让它跳过对汉堡元素的点击检测:
// use-clicked-outside.js export default function useClickedOutside(ref, callback, excludeRef) { useEffect(() => { function handleClickOutside(event) { // 如果点击目标是excludeRef指向的元素,直接返回不执行关闭逻辑 if (excludeRef?.current?.contains(event.target)) { return; } if (ref.current && !ref.current.contains(event.target)) { callback(); } } document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, [ref, excludeRef]); }
然后在SideNav中传入汉堡的ref:
// SideNav.jsx const toggleRef = React.createRef(); useClickedOutside(sideRef, () => setSideIsOpen(false), toggleRef); // 把ref传递给SideToggle组件 <SideToggle ref={toggleRef} sideIsOpen={sideIsOpen} sideNavToggle={sideNavToggle} />
这个方案也能避免两个状态逻辑的冲突,不过你已经用移到内部的方式解决了冲突,这个可以作为备选思路。
内容的提问来源于stack exchange,提问作者Neil

