mui / mui/base-ui

[NavigationMenu] Maximum update depth exceeded for NavigationMenu.Content with suspending child

Open
#5,103 1 comment 0 reactions 0 assignees View on GitHub
component: navigation menu has workaround type: bug
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.