Bug report
Current behavior
When a NavigationMenu.Content contains a child that suspends on first render (e.g., a React.lazy
component or a suspending query) and the next Suspense boundary is above the
NavigationMenu, hovering a trigger to open its popup for the first time throws
Maximum update depth exceeded.
The error seems to come from the composite list trigger's flushSync on open:
dispatchSetState
Object.callback (CompositeList.mjs:45)
useCompositeListItem.mjs:59
commitHookEffectListUnmount
...
NavigationMenuTrigger.handleOpenChange (-> ReactDOM.flushSync)
useHoverReferenceInteraction (handleMouseMove)
A single static item does not reproduce it, several mapped NavigationMenu.Items are needed.
Expected behavior
Opening the popup should suspend gracefully (resolving to the loaded content once ready) without an
infinite update loop.
Reproducible example
Hover over one of the navigation items in this Stackblitz
This is the code snippet:
import { NavigationMenu } from '@base-ui/react/navigation-menu';
import * as React from 'react';
const cache = new Map<string, { done: boolean; promise: Promise<void> }>();
function suspendOnce(key: string): void {
let entry = cache.get(key);
if (!entry) {
const created = { done: false, promise: Promise.resolve() };
created.promise = new Promise<void>((resolve) => {
setTimeout(() => { created.done = true; resolve(); }, 150);
});
cache.set(key, created);
entry = created;
}
if (!entry.done) throw entry.promise;
}
function SuspendingContent({ id }: { id: string }) {
suspendOnce(id);
return <span>content {id}</span>;
}
const ITEMS = ['a', 'b', 'c', 'd', 'e'];
export default function App() {
return (
<React.Suspense fallback={<div>loading…</div>}>
<NavigationMenu.Root>
<NavigationMenu.List>
{ITEMS.map((id) => (
<NavigationMenu.Item key={id}>
<NavigationMenu.Trigger>{id}</NavigationMenu.Trigger>
<NavigationMenu.Content>
<SuspendingContent id={id} />
</NavigationMenu.Content>
</NavigationMenu.Item>
))}
</NavigationMenu.List>
<NavigationMenu.Portal>
<NavigationMenu.Positioner>
<NavigationMenu.Popup>
<NavigationMenu.Viewport />
</NavigationMenu.Popup>
</NavigationMenu.Positioner>
</NavigationMenu.Portal>
</NavigationMenu.Root>
</React.Suspense>
);
}
Base UI version
v1.4.1, v1.6.0
Which browser are you using?
Chrome
Which OS are you using?
macOS
Additional context
Wrapping the suspending child in its own Suspense boundary inside the Content works around the issue.
Looks related to #3700 ("Fix Maximum update depth exceeded error with Suspense").
Bug report
Current behavior
When a
NavigationMenu.Contentcontains a child that suspends on first render (e.g., aReact.lazycomponent or a suspending query) and the next
Suspenseboundary is above theNavigationMenu, hovering a trigger to open its popup for the first time throwsMaximum update depth exceeded.The error seems to come from the composite list trigger's
flushSyncon open:A single static item does not reproduce it, several mapped
NavigationMenu.Items are needed.Expected behavior
Opening the popup should suspend gracefully (resolving to the loaded content once ready) without an
infinite update loop.
Reproducible example
Hover over one of the navigation items in this Stackblitz
This is the code snippet:
Base UI version
v1.4.1, v1.6.0
Which browser are you using?
Chrome
Which OS are you using?
macOS
Additional context
Wrapping the suspending child in its own
Suspenseboundary inside theContentworks around the issue.Looks related to #3700 ("Fix
Maximum update depth exceedederror with Suspense").