弹出菜单呈现在与锚元素不同的位置

Ota*_*der 5 javascript reactjs material-ui

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

错误的位置

头像是右侧绿色的“OB”按钮。没有控制台错误并检查Popover元素,它正在接收道具anchorEl

弹出道具

头像右侧的语言菜单渲染得很好,在应该打开的地方打开。我的代码看起来不错,我真的不知道为什么这个位置是错误的:

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 />;
}
Run Code Online (Sandbox Code Playgroud)

我正在使用material-ui-popup-state,但我尝试在没有这个包的情况下“现场”实现,结果是相同的。

对此的任何帮助表示赞赏。提前致谢

Rya*_*ell 2

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

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

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

每当根元素具有不同类型时,React 就会拆除旧树并从头开始构建新树。从<a><img>、或从<Article><Comment>、或从<Button><div>- 其中任何一个都将导致完全重建。

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

如果您将DesktopNavbar和移动MobileNavbar到顶层并从DashboardNavbarprops 传递任何依赖项,这将导致DesktopNavbarReact 将 和 重新渲染为一致的组件类型DashboardNavbarLanguageMenu没有同样的问题,因为大概它的状态是内部管理的,所以打开它不会导致DashboardNavbar.

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

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} />;
}
Run Code Online (Sandbox Code Playgroud)

解决此问题的另一种方法是消除嵌套组件,这样它DashboardNavbar就是一个组件:

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}
                />
            </>
    }
    </>;
}
Run Code Online (Sandbox Code Playgroud)

相关回答: