Popover menu renders in different place than the anchor element

前端 未结 1 1351
春和景丽
春和景丽 2021-01-20 00:58

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:

The avatar

相关标签:
1条回答
  • 2021-01-20 01:53

    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
    0 讨论(0)
提交回复
热议问题