Skip to content

Commit 105e8f6

Browse files
authored
[Dialog] Fix Maximum update depth exceeded error with Suspense (#3700)
1 parent e730a6e commit 105e8f6

File tree

3 files changed

+53
-1
lines changed

3 files changed

+53
-1
lines changed

packages/react/src/dialog/portal/DialogPortal.test.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as React from 'react';
22
import { Dialog } from '@base-ui/react/dialog';
33
import { createRenderer, describeConformance } from '#test-utils';
4+
import { expect } from 'vitest';
5+
import { screen } from '@mui/internal-test-utils';
46

57
describe('<Dialog.Portal />', () => {
68
const { render } = createRenderer();
@@ -11,4 +13,44 @@ describe('<Dialog.Portal />', () => {
1113
return render(<Dialog.Root open>{node}</Dialog.Root>);
1214
},
1315
}));
16+
17+
describe('Suspense integration', () => {
18+
// Issue #3695
19+
it('should not throw "Maximum update depth exceeded" when Suspense boundary is outside Portal', async () => {
20+
function createLazyComponent() {
21+
let resolvePromise: ((value: { default: React.ComponentType }) => void) | null = null;
22+
const promise = new Promise<{ default: React.ComponentType }>((resolve) => {
23+
resolvePromise = resolve;
24+
});
25+
26+
return {
27+
LazyComponent: React.lazy(() => promise),
28+
resolve(value: { default: React.ComponentType }) {
29+
if (!resolvePromise) {
30+
throw new Error('Lazy message resolver not initialized.');
31+
}
32+
resolvePromise(value);
33+
},
34+
};
35+
}
36+
37+
const { LazyComponent, resolve } = createLazyComponent();
38+
39+
await render(
40+
<React.Suspense fallback="Loading...">
41+
<Dialog.Root open>
42+
<Dialog.Portal>
43+
<Dialog.Popup>
44+
<LazyComponent />
45+
</Dialog.Popup>
46+
</Dialog.Portal>
47+
</Dialog.Root>
48+
</React.Suspense>,
49+
);
50+
51+
expect(await screen.findByText('Loading...')).not.to.equal(null);
52+
resolve({ default: () => <p>Greetings</p> });
53+
expect(await screen.findByText('Greetings')).not.to.equal(null);
54+
});
55+
});
1456
});

packages/react/src/dialog/portal/DialogPortal.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const DialogPortal = React.forwardRef(function DialogPortal(
2222
const { store } = useDialogRootContext();
2323
const mounted = store.useState('mounted');
2424
const modal = store.useState('modal');
25+
const open = store.useState('open');
2526

2627
const shouldRender = mounted || keepMounted;
2728
if (!shouldRender) {

packages/react/src/floating-ui-react/components/FloatingPortal.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as ReactDOM from 'react-dom';
44
import { isNode } from '@floating-ui/utils/dom';
55
import { useId } from '@base-ui/utils/useId';
66
import { useIsoLayoutEffect } from '@base-ui/utils/useIsoLayoutEffect';
7+
import { useStableCallback } from '@base-ui/utils/useStableCallback';
78
import { FocusGuard } from '../../utils/FocusGuard';
89
import {
910
enableFocusInside,
@@ -72,6 +73,14 @@ export function useFloatingPortalNode(
7273
null,
7374
);
7475
const [portalNode, setPortalNode] = React.useState<HTMLElement | null>(null);
76+
const setPortalNodeRef = useStableCallback((node: HTMLElement | null) => {
77+
if (node !== null) {
78+
// the useIsoLayoutEffect below watching containerProp / parentPortalNode
79+
// sets setPortalNode(null) when the container becomes null or changes.
80+
// So even though the ref callback now ignores null, the portal node still gets cleared.
81+
setPortalNode(node);
82+
}
83+
});
7584

7685
const containerRef = React.useRef<HTMLElement | ShadowRoot | null>(null);
7786

@@ -113,7 +122,7 @@ export function useFloatingPortalNode(
113122
}, [containerProp, parentPortalNode, uniqueId]);
114123

115124
const portalElement = useRenderElement('div', componentProps, {
116-
ref: [ref, setPortalNode],
125+
ref: [ref, setPortalNodeRef],
117126
state: elementState,
118127
props: [
119128
{

0 commit comments

Comments
 (0)