【问题标题】:Deeper understanding of React event bubbling / propagation and state management更深入地理解 React 事件冒泡/传播和状态管理
【发布时间】:2021-09-13 03:10:03
【问题描述】:

如果这是一个愚蠢的问题,我提前道歉。虽然我已经设法让它工作了,但我想更深入地了解它。

我正在 react 中构建一个自定义汉堡菜单,当您单击无序列表或汉堡图标本身之外的任何位置时,该菜单就会关闭。

我在这里看到了答案Detect click outside React component 而且我已经关注了它,但我不明白为什么它不起作用。

首先,当它只是汉堡图标并且没有在菜单外单击以关闭选项时,它工作得很好。 然后,当我使用 useRef 挂钩获取对无序列表的引用以便仅在未单击列表时关闭菜单时,除了单击实际的汉堡包图标外,它工作得很好。

经过大量业余调试后,我终于意识到发生了什么。 首先,当我打开菜单时,状态 showMenu 变为 true, 然后当我点击汉堡图标关闭时, 父包装元素首先触发而不是汉堡菜单,这很奇怪,因为在冒泡阶段我希望最里面的元素首先触发。

所以父元素会关闭菜单改变状态,导致组件重新渲染。然后,当事件到达实际图标时,handleClick 将再次将状态切换为 true,给人一种汉堡点击不起作用的印象。

我设法通过在父元素上使用 event.stopPropogation() 来解决这个问题。 但这似乎很奇怪,因为我不希望父元素的点击首先触发,尤其是在我使用冒泡阶段时。 我唯一能想到的是因为它是一个原生 dom addeventlistener 事件,它在合成事件之前首先触发。

下面是带有汉堡包的移动导航的代码 header 组件根据屏幕宽度呈现正常的 Nav 或 MobileNav。我没有为高阶组件添加代码以使其更容易通过,但如果需要,我可以提供所有代码:

//MobileNav.js
export default function MobileNav() {

    const [showMenu, setShowMenu] = useState(false);
    const ulRef = useRef();

    console.log('State when Mobilenav renders: ', showMenu);

    useEffect(() => {
        
        let handleMenuClick = (event) => {

            console.log('App Clicked!');

            if(ulRef.current && !ulRef.current.contains(event.target)){
                setShowMenu(false);
                event.stopPropagation();
            }
        }

        document.querySelector('#App').addEventListener('click', handleMenuClick);

        return () => {
            document.querySelector('#App').removeEventListener('click', handleMenuClick);
        }
    }, [])

    return (
        <StyledMobileNav>
            <PersonOutlineIcon />
            <MenuIcon showMenu={showMenu} setShowMenu={setShowMenu} />

            {
                (showMenu) &&
                    <ul ref={ulRef} style={{
                            backgroundColor: 'green',
                            opacity: '0.7',
                            position: 'absolute',
                            top: 0,
                            right: 0,
                            padding: '4em 1em 1em 1em',
                        }}
                    >

                        <MenuList/>
                    </ul>
            }

            
        </StyledMobileNav>
    )
}

//MenuIcon.js

/**
* By putting the normal span instead of the MenuLine component after > worked in order to hover all div's
*/
const MenuWrap = styled.div`
    width: 28px;
    position: relative;
    transform: ${(props) => props.showMenu ? `rotate(-180deg)` : `none` };
    transition: transform 0.2s ease;
    z-index: 2;

    &:hover > div{
        background-color: white;
    }
`;

const MenuLine = styled.div`
    width: 100%;
    height: 2px;
    position: relative;
    transition: transform 0.2s ease;
    background-color: ${(props) => props.showMenu ? 'white' : mainBlue};
    &:hover {
        background-color: white;
    }
`;

const TopLine = styled(MenuLine)`
    ${(props) => {
        let style = `margin-bottom: 7px;`;
        if(props.showMenu){
            style += `top: 9px; transform: rotate(45deg);`;
        }
        return style;
    }}
`;

const MidLine = styled(MenuLine)`
    ${(props) => {
        let style = `margin-bottom: 7px;`;
        if(props.showMenu){
            style += `opacity: 0;`;
        }
        return style;
    }}
`;

const BottomLine = styled(MenuLine)`
    ${props => {
        if(props.showMenu){
           return `bottom: 9px; transform: rotate(-45deg);`;
        }
    }}
`;

export default function MenuIcon({showMenu, setShowMenu}) {

    const handleMenuClick = (event) => {
        console.log('Menu Clicked!');
        console.log('State before change Icon: ', showMenu);
        setShowMenu(!showMenu);
    }

    return (
        <MenuWrap onClick={handleMenuClick} showMenu={showMenu}>
            <TopLine onClick={handleMenuClick} showMenu={showMenu}></TopLine>
            <MidLine onClick={handleMenuClick} showMenu={showMenu}></MidLine>
            <BottomLine onClick={handleMenuClick} showMenu={showMenu}></BottomLine>
        </MenuWrap>
    )
}

阅读这篇文章 https://dev.to/eladtzemach/event-capturing-and-bubbling-in-react-2ffg 基本上它表明 React 中的事件与 DOM 事件的工作方式基本相同

但由于某种原因,事件冒泡无法正常工作 请参阅下面显示状态如何变化的屏幕截图:

谁能解释为什么会发生这种情况或出了什么问题?

【问题讨论】:

    标签: javascript reactjs event-handling react-state event-propagation


    【解决方案1】:

    这是竞争事件侦听器的常见问题。看来您已经解决了问题是点击关闭处理和菜单按钮点击关闭处理都是同时触发并相互取消的。

    应该按照 DOM3 规范的附加顺序调用事件侦听器,但是旧版浏览器可能无法实现此规范(请参阅此问题:The Order of Multiple Event Listeners)。在您的情况下,首先附加点击监听器(在 &lt;MobileNav&gt; 组件中)(因为您在那里使用 addEventListener,而孩子使用 React onClick 道具)。

    而不是依赖于添加事件侦听器的顺序(这可能会变得棘手),您应该更新您的代码,以便触发器不会同时发生(这是此答案概述的方法)或其他处理程序中的逻辑不重叠。

    解决方案:

    如果您将 ref'd 元素向上移动一个级别,使其同时包含菜单按钮和菜单本身,您可以避免重叠/竞争事件。

    这样,菜单按钮位于忽略点击的空间内,因此在单击菜单按钮时不会触发外部单击侦听器(点击退出侦听器),但如果用户单击菜单外的任何位置,则会触发或其按钮。

    例如:

    return (
        <StyledMobileNav>
            <PersonOutlineIcon />
            <div ref={menuRef}>
                <MenuIcon showMenu={showMenu} setShowMenu={setShowMenu} />
                { showMenu && (
                    <ul>
                        <MenuList/>
                    </ul>
                )}
            </div>
        </StyledMobileNav>
    )
    

    然后使用menuRef 来检查外部点击。

    作为附加建议,尝试将所有菜单逻辑放入一个组件中以更好地组织,例如:

    function Menu() {
        const [showMenu, setShowMenu] = React.useState(false);
    
        // click out handling here
        
        return (
            <div ref={menuRef}>
                <MenuIcon showMenu={showMenu} setShowMenu={setShowMenu} />
                { showMenu && (
                    <ul>
                        <MenuList/>
                    </ul>
                )}
            </div>
        )
    }
    

    【讨论】:

    • 我现在弄清楚并修复了它,抱歉有点慢大声笑,非常感谢
    猜你喜欢
    • 1970-01-01
    • 2017-12-26
    • 2011-12-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-10-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多