Skip to content

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

Description

@Stef2k16

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").

Metadata

Metadata

Assignees

No one assigned

    Labels

    component: navigation menuChanges related to the navigation menu component.has workaroundThere’s a bug, but users have a complete workaround, so no urgent fix or release is needed.type: bugIt doesn't behave as expected.

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions