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

IT技术 javascript reactjs material-ui
2021-04-07 07:39:29

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

错误的位置

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

弹出props

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

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,但我尝试在没有这个包的情况下实现“手头”,结果是一样的。

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

1个回答

问题是嵌套DesktopNavbarDashboardNavbar这意味着每次DashboardNavbar重新渲染时,DesktopNavbar都会重新定义。由于DesktopNavbar与之前的 render 相比将是一个新函数DashboardNavbar,React 不会将其识别为相同的组件类型,并且DesktopNavbar将重新挂载而不是重新渲染。由于菜单状态保持在 内DashboardNavbar,因此打开菜单会导致重新渲染,DashboardNavbar因此重新定义DesktopNavbarso,由于重新安装了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 的一部分。

如果您改为将DesktopNavbarMobileNavbar移到顶层并从DashboardNavbaras props传递任何依赖项,这将导致DesktopNavbarReact其识别为跨DashboardNavbar. LanguageMenu没有同样的问题,因为大概它的状态是在内部管理的,所以打开它不会导致重新渲染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} />;
}

解决此问题的另一种方法是消除嵌套组件,因此它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}
                />
            </>
    }
    </>;
}

相关回答:

@OtavioBonder 我在答案中添加了更多解释。
2021-05-29 07:39:29
是的,你明白了。我可以解决将状态传递到DashboardMenu. 我正在从这个文件中导出MainNavbar(将在/dashboard路由之外呈现)和DashboardNavbar,这就是我嵌套定义组件的原因。我应该拆分文件以MainNavbar在一个文件和DashboardNavbar另一个文件中定义,还是可以继续嵌套组件?
2021-06-04 07:39:29
@OtavioBonder 抱歉,但我不太了解您对嵌套组件定义的原因的解释。可以将任意数量的组件放在同一个文件的顶层,但您不应该将组件函数嵌套在彼此内部。我的第二个解决方案是最简单的,因为它保持整体结构基本相同。
2021-06-10 07:39:29
我知道可以将组件放在顶层,我嵌套它们以便使代码更有条理,将属于该组件的功能放在该组件中。在性能方面,嵌套组件对性能有影响吗?很抱歉打扰你,但这是一个诚实的问题,我对反应没有太多经验
2021-06-14 07:39:29
谢谢,我决定拆分文件并将组件移到顶部。谢谢详细的解释
2021-06-17 07:39:29