[NavigationMenu] Maximum update depth exceeded for NavigationMenu.Content with suspending child
- Dominant language
- TypeScript
- Stars
- 10.9k
- Forks
- 543
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 101
Description
# 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.Item`s 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](https://stackblitz.com/edit/vitejs-vite-939wccuh?file=src%2FApp.tsx)
This is the code snippet:
```tsx
import { NavigationMenu } from '@base-ui/react/navigation-menu';
import * as React from 'react';
const cache = new Map }>();
function suspendOnce(key: string): void {
let entry = cache.get(key);
if (!entry) {
const created = { done: false, promise: Promise.resolve() };
created.promise = new Promise((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 content {id};
}
const ITEMS = ['a', 'b', 'c', 'd', 'e'];
export default function App() {
return (
loading…}>
{ITEMS.map((id) => (
{id}
))}
);
}
```
## 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").
Contributor guide
Research direction
Reproduce the issue with the linked StackBlitz example using several mapped NavigationMenu.Items and a suspending child. Start by reading CompositeList.mjs, useCompositeListItem.mjs, and NavigationMenuTrigger.handleOpenChange around the flushSync call; compare the behavior with an inner Suspense boundary. Done means the popup opens and resolves its content without a maximum update depth error.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100