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
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
to
, or from
to
, or from
to
- any of those will lead to a full rebuild.When you redefine
DesktopNavbar
andMobileNavbar
on re-render ofDashboardNavbar
, 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
andMobileNavbar
to the top-level and pass any dependencies fromDashboardNavbar
as props, this will causeDesktopNavbar
to be recognized by React as a consistent component type across re-renders ofDashboardNavbar
.LanguageMenu
doesn't have the same issue, because presumably its state is managed internally, so opening it doesn't cause a re-render ofDashboardNavbar
.Sample restructuring of code (not executed, so may have minor errors):
function DesktopNavbar({configMenuState, i18n}) { return ( <>
> ); } function MobileNavbar({setDrawer, configDrawer, setConfigDrawer, avatarId}) { return ( <> {avatarId} setDrawer(true)} /> setConfigDrawer(true)} >{avatarId} > ); } 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 ? : ; } 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 &&
} {!useDesktopLayout && <> {avatarId} setDrawer(true)} /> setConfigDrawer(true)} >{avatarId} > } >; } 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)