弹出菜单与锚元素的渲染位置不同 [英] Popover menu renders in different place than the anchor element

查看:32
本文介绍了弹出菜单与锚元素的渲染位置不同的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在实现一个菜单,当用户单击头像时该菜单将打开.问题在于菜单在完全不同的位置呈现:

I'm implementing a menu that opens when the user clicks on an Avatar. The problem is that the menu is rendering in a completely different place:

该头像是右边的绿色"OB"按钮.没有控制台错误,正在检查 Popover 元素,它正在接收 anchorEl 道具:

The avatar is the green "OB" button on the right. There is no console error and inspecting the Popover element, it's receiving the anchorEl prop:

化身右侧的语言菜单可以很好地渲染,并在应打开的位置打开.我的代码看起来不错,我真的不确定为什么位置错误:

The language menu, on the right of the avatar, renders just fine, opening where it should open. My code seems fine, I'm really not sure why the position is wrong:

export function DashboardNavbar({ setDrawer }) {
    // translation hook
    const { i18n } = useTranslation("navbar");

    // config drawer state
    const [configDrawer, setConfigDrawer] = useState(false);

    // config menu state
    const configMenuState = usePopupState({
        variant: "popover",
        popupId: "configMenu"
    });

    // avatar id
    const [cookie] = useCookies("userInfo");
    const decodedToken = decodeToken(cookie.userInfo.token);
    const avatarId =
        decodedToken.firstName.charAt(0) + decodedToken.lastName.charAt(0);

    function DesktopNavbar() {
        return (
            <>
                <StyledDashboardNavbar>
                    <Container maxWidth="lg">
                        <div
                            style={{
                                display: "flex",
                                justifyContent: "flex-end"
                            }}
                        >
                            <Avatar
                                style={{
                                    backgroundColor:
                                        theme.palette.secondary.main
                                }}
                                {...bindTrigger(configMenuState)}
                            >
                                {avatarId}
                            </Avatar>
                            <DashboardMenu
                                bindMenu={bindMenu}
                                menuState={configMenuState}
                            />
                            <LanguageMenu i18n={i18n} />
                        </div>
                    </Container>
                </StyledDashboardNavbar>
            </>
        );
    }

    function MobileNavbar() {
        return (
            <>
                <StyledDashboardNavbar>
                    <Container maxWidth="md">
                        <div className="navbar">
                            <div
                                style={{
                                    display: "flex",
                                    alignItems: "center"
                                }}
                            >
                                <MenuIcon
                                    color="secondary"
                                    onClick={() => setDrawer(true)}
                                />
                            </div>
                            <div
                                className="logo"
                                onClick={() => setConfigDrawer(true)}
                            >
                                <Avatar
                                    style={{
                                        backgroundColor:
                                            theme.palette.secondary.main
                                    }}
                                >
                                    {avatarId}
                                </Avatar>
                            </div>
                        </div>
                    </Container>
                </StyledDashboardNavbar>
                <AvatarDrawer
                    drawer={configDrawer}
                    setDrawer={setConfigDrawer}
                />
            </>
        );
    }

    return window.innerWidth > 480 ? <DesktopNavbar /> : <MobileNavbar />;
}

我正在使用 material-ui-popup-state ,但是我尝试在没有此程序包的情况下实现"on-hand",结果是相同的.

I'm using the material-ui-popup-state, but I tried implementing "on-hand" without this package and the result was the same.

对此有任何帮助,我们深表感谢.预先感谢

Any help on this is appreciated. Thanks in advance

推荐答案

问题是 DesktopNavbar DashboardNavbar 中的嵌套.这意味着每次 DashboardNavbar 重新渲染时,都会重新定义 DesktopNavbar .由于与以前的 DashboardNavbar 渲染相比, DesktopNavbar 将是一个新功能,因此React无法将其识别为相同的组件类型,而 DesktopNavbar 将是相同的组件类型.重新安装而不是仅重新渲染.由于菜单状态保持在 DashboardNavbar 内,因此打开菜单会导致重新呈现 DashboardNavbar ,因此会重新定义 DesktopNavbar ,因此,由于重新安装了 DesktopNavbar 及其内部的所有内容,传递给菜单的锚元素将不再是DOM的一部分.

The problem is the nesting of DesktopNavbar within DashboardNavbar. This means that every time DashboardNavbar re-renders, DesktopNavbar will be redefined. Since DesktopNavbar will be a new function compared to the previous render of DashboardNavbar, React will not recognize it as the same component type and DesktopNavbar will be re-mounted rather than just re-rendered. Since the menu state is maintained within DashboardNavbar, opening the menu causes a re-render of DashboardNavbar and therefore a re-definition of DesktopNavbar so, due to the re-mounting of DesktopNavbar and everything inside it, the anchor element passed to the menu will no longer be part of the DOM.

嵌套组件的定义几乎总是一个坏主意,因为每次重新渲染包含的组件时,嵌套的组件都会被视为一种新的元素类型.

It is almost always a bad idea to nest the definitions of components, because the nested components will be treated as a new element type with each re-render of the containing component.

来自 https://reactjs.org/docs/reconciliation.html#elements-of-different-types :

每当根元素具有不同类型时,React都会拆除旧树并从头开始构建新树.从< a> 移至< img> ,或从< Article> 移至< Comment> ,或从< Button> < div> -任何这些都将导致完全重建.

Whenever the root elements have different types, React will tear down the old tree and build the new tree from scratch. Going from <a> to <img>, or from <Article> to <Comment>, or from <Button> to <div> - any of those will lead to a full rebuild.

在重新渲染 DashboardNavbar 时重新定义 DesktopNavbar MobileNavbar 时,这些元素中的整个DOM元素树将从中删除.并从头开始重新创建DOM,而不仅仅是将更改应用于现有DOM元素.这会对性能产生重大影响,还会引起行为问题,例如您遇到的行为问题,即您所引用的元素意外地不再是DOM的一部分.

When you redefine DesktopNavbar and MobileNavbar on re-render of DashboardNavbar, the entire tree of DOM elements within those will be removed from the DOM and re-created from scratch rather than just applying changes to the existing DOM elements. This has a big performance impact and also causes behavior issues like the one you experienced where elements that you are referring to are unexpectedly no longer part of the DOM.

如果您改为将 DesktopNavbar MobileNavbar 移至顶层,并将 DashboardNavbar 中的所有依赖项作为道具传递,则会导致 将被React识别为跨 DashboardNavbar 的重新渲染的一致组件类型. LanguageMenu 没有相同的问题,因为它的状态大概是在内部管理的,因此打开它不会导致 DashboardNavbar 的重新呈现.

If you instead move DesktopNavbar and MobileNavbar to the top-level and pass any dependencies from DashboardNavbar as props, this will cause DesktopNavbar to be recognized by React as a consistent component type across re-renders of DashboardNavbar. LanguageMenu doesn't have the same issue, because presumably its state is managed internally, so opening it doesn't cause a re-render of DashboardNavbar.

示例代码重组(未执行,因此可能会有小错误):

Sample restructuring of code (not executed, so may have minor errors):

function DesktopNavbar({configMenuState, i18n}) {
    return (
        <>
            <StyledDashboardNavbar>
                <Container maxWidth="lg">
                    <div
                        style={{
                            display: "flex",
                            justifyContent: "flex-end"
                        }}
                    >
                        <Avatar
                            style={{
                                backgroundColor:
                                    theme.palette.secondary.main
                            }}
                            {...bindTrigger(configMenuState)}
                        >
                            {avatarId}
                        </Avatar>
                        <DashboardMenu
                            bindMenu={bindMenu}
                            menuState={configMenuState}
                        />
                        <LanguageMenu i18n={i18n} />
                    </div>
                </Container>
            </StyledDashboardNavbar>
        </>
    );
}

function MobileNavbar({setDrawer, configDrawer, setConfigDrawer, avatarId}) {
    return (
        <>
            <StyledDashboardNavbar>
                <Container maxWidth="md">
                    <div className="navbar">
                        <div
                            style={{
                                display: "flex",
                                alignItems: "center"
                            }}
                        >
                            <MenuIcon
                                color="secondary"
                                onClick={() => setDrawer(true)}
                            />
                        </div>
                        <div
                            className="logo"
                            onClick={() => setConfigDrawer(true)}
                        >
                            <Avatar
                                style={{
                                    backgroundColor:
                                        theme.palette.secondary.main
                                }}
                            >
                                {avatarId}
                            </Avatar>
                        </div>
                    </div>
                </Container>
            </StyledDashboardNavbar>
            <AvatarDrawer
                drawer={configDrawer}
                setDrawer={setConfigDrawer}
            />
        </>
    );
}

export function DashboardNavbar({ setDrawer }) {
    // translation hook
    const { i18n } = useTranslation("navbar");

    // config drawer state
    const [configDrawer, setConfigDrawer] = useState(false);

    // config menu state
    const configMenuState = usePopupState({
        variant: "popover",
        popupId: "configMenu"
    });

    // avatar id
    const [cookie] = useCookies("userInfo");
    const decodedToken = decodeToken(cookie.userInfo.token);
    const avatarId =
        decodedToken.firstName.charAt(0) + decodedToken.lastName.charAt(0);


    return window.innerWidth > 480 ? <DesktopNavbar configMenuState={configMenuState} i18n={i18n} /> : <MobileNavbar setDrawer={setDrawer} configDrawer={configDrawer} setConfigDrawer={setConfigDrawer} avatarId={avatarId} />;
}

解决此问题的另一种方法是只消除嵌套组件,以使 DashboardNavbar 是单个组件:

An alternative way to fix this is to just eliminate the nested components, so that DashboardNavbar is a single component:

export function DashboardNavbar({ setDrawer }) {
    // translation hook
    const { i18n } = useTranslation("navbar");

    // config drawer state
    const [configDrawer, setConfigDrawer] = useState(false);

    // config menu state
    const configMenuState = usePopupState({
        variant: "popover",
        popupId: "configMenu"
    });

    // avatar id
    const [cookie] = useCookies("userInfo");
    const decodedToken = decodeToken(cookie.userInfo.token);
    const avatarId =
        decodedToken.firstName.charAt(0) + decodedToken.lastName.charAt(0);
    const useDesktopLayout = window.innerWidth > 480;
    return <>    
    {useDesktopLayout && 
                <StyledDashboardNavbar>
                    <Container maxWidth="lg">
                        <div
                            style={{
                                display: "flex",
                                justifyContent: "flex-end"
                            }}
                        >
                            <Avatar
                                style={{
                                    backgroundColor:
                                        theme.palette.secondary.main
                                }}
                                {...bindTrigger(configMenuState)}
                            >
                                {avatarId}
                            </Avatar>
                            <DashboardMenu
                                bindMenu={bindMenu}
                                menuState={configMenuState}
                            />
                            <LanguageMenu i18n={i18n} />
                        </div>
                    </Container>
                </StyledDashboardNavbar>
    }

    {!useDesktopLayout && 
            <>
                <StyledDashboardNavbar>
                    <Container maxWidth="md">
                        <div className="navbar">
                            <div
                                style={{
                                    display: "flex",
                                    alignItems: "center"
                                }}
                            >
                                <MenuIcon
                                    color="secondary"
                                    onClick={() => setDrawer(true)}
                                />
                            </div>
                            <div
                                className="logo"
                                onClick={() => setConfigDrawer(true)}
                            >
                                <Avatar
                                    style={{
                                        backgroundColor:
                                            theme.palette.secondary.main
                                    }}
                                >
                                    {avatarId}
                                </Avatar>
                            </div>
                        </div>
                    </Container>
                </StyledDashboardNavbar>
                <AvatarDrawer
                    drawer={configDrawer}
                    setDrawer={setConfigDrawer}
                />
            </>
    }
    </>;
}

相关答案:

登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆