Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Popover menu renders in different place than the anchor element

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:

wrong location

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:

popover props

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 />;
}

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

like image 822
Otavio Bonder Avatar asked Nov 06 '22 09:11

Otavio Bonder


1 Answers

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.

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

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.

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.

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} />;
}

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}
                />
            </>
    }
    </>;
}

Related answers:

  • React Material-UI menu anchor broken by react-window list
  • React/MUI Popover positioning incorrectly with anchorPosition
  • Material UI Popover is thrown to the top left when used on an inline button in a table with a unique key
like image 166
Ryan Cogswell Avatar answered Nov 15 '22 07:11

Ryan Cogswell