From 79ff3305c8f8de9e27ccef2002f5876228c82170 Mon Sep 17 00:00:00 2001 From: martincupela Date: Thu, 9 Apr 2026 16:04:18 +0200 Subject: [PATCH 1/2] fix(dialog): keep DialogManagerProvider mounted on first render and stabilize manager identity --- .../hooks/useSelectedChannelState.ts | 2 +- .../__tests__/DialogManagerContext.test.js | 15 ++++++ src/context/DialogManagerContext.tsx | 48 ++++++++++++------- src/store/hooks/useStateStore.ts | 6 ++- 4 files changed, 52 insertions(+), 19 deletions(-) diff --git a/src/components/ChannelList/hooks/useSelectedChannelState.ts b/src/components/ChannelList/hooks/useSelectedChannelState.ts index 53ff0c1041..ea4c2343fa 100644 --- a/src/components/ChannelList/hooks/useSelectedChannelState.ts +++ b/src/components/ChannelList/hooks/useSelectedChannelState.ts @@ -45,5 +45,5 @@ export function useSelectedChannelState({ return selector(channel); }, [channel, selector]); - return useSyncExternalStore(subscribe, getSnapshot); + return useSyncExternalStore(subscribe, getSnapshot, getSnapshot); } diff --git a/src/components/Dialog/__tests__/DialogManagerContext.test.js b/src/components/Dialog/__tests__/DialogManagerContext.test.js index cd87b32fc7..b6b76d2022 100644 --- a/src/components/Dialog/__tests__/DialogManagerContext.test.js +++ b/src/components/Dialog/__tests__/DialogManagerContext.test.js @@ -1,5 +1,6 @@ import React from 'react'; import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { renderToStaticMarkup } from 'react-dom/server'; import { DialogManagerProvider, useDialogManager, @@ -8,6 +9,10 @@ import { import '@testing-library/jest-dom'; import { useDialogIsOpen, useOpenedDialogCount } from '../hooks'; +jest.mock('../../../components/Dialog/DialogPortal', () => ({ + DialogPortalDestination: () => null, +})); + const TEST_IDS = { CLOSE_DIALOG: 'close-dialog', DIALOG_COUNT: 'dialog-count', @@ -90,6 +95,16 @@ describe('DialogManagerContext', () => { expect(screen.getByTestId(TEST_IDS.DIALOG_COUNT).textContent).toBe('0'); }); + it('renders children during SSR when id is provided', () => { + const html = renderToStaticMarkup( + +
server-rendered-child
+
, + ); + + expect(html).toContain('server-rendered-child'); + }); + it('provides dialog manager to non-child components', () => { render( diff --git a/src/context/DialogManagerContext.tsx b/src/context/DialogManagerContext.tsx index c3a57533b7..311206e264 100644 --- a/src/context/DialogManagerContext.tsx +++ b/src/context/DialogManagerContext.tsx @@ -15,19 +15,12 @@ type DialogManagerId = string; type DialogManagersState = Record; const dialogManagersRegistry: StateStore = new StateStore({}); +const pendingDialogManagersById: Partial> = {}; +const dialogManagerMountCountsById: Partial> = {}; const getDialogManager = (id: string): DialogManager | undefined => dialogManagersRegistry.getLatestValue()[id]; -const getOrCreateDialogManager = (id: string) => { - let manager = getDialogManager(id); - if (!manager) { - manager = new DialogManager({ id }); - dialogManagersRegistry.partialNext({ [id]: manager }); - } - return manager; -}; - const removeDialogManager = (id: string) => { if (!getDialogManager(id)) return; dialogManagersRegistry.partialNext({ [id]: undefined }); @@ -51,23 +44,44 @@ export const DialogManagerProvider = ({ children, id, }: PropsWithChildren<{ id?: string }>) => { - const [dialogManager, setDialogManager] = useState(() => { - if (id) return getDialogManager(id) ?? null; + const [dialogManager, setDialogManager] = useState(() => { + if (id) { + const manager = + getDialogManager(id) ?? + pendingDialogManagersById[id] ?? + new DialogManager({ id }); + pendingDialogManagersById[id] = manager; + return manager; + } + return new DialogManager(); // will not be included in the registry }); useEffect(() => { if (!id) return; - setDialogManager(getOrCreateDialogManager(id)); + const manager = + getDialogManager(id) ?? pendingDialogManagersById[id] ?? new DialogManager({ id }); + + if (!getDialogManager(id)) { + dialogManagersRegistry.partialNext({ [id]: manager }); + } + delete pendingDialogManagersById[id]; + + setDialogManager((prev) => (prev === manager ? prev : manager)); + dialogManagerMountCountsById[id] = (dialogManagerMountCountsById[id] ?? 0) + 1; + return () => { + const nextMountCount = (dialogManagerMountCountsById[id] ?? 1) - 1; + if (nextMountCount > 0) { + dialogManagerMountCountsById[id] = nextMountCount; + return; + } + + delete dialogManagerMountCountsById[id]; removeDialogManager(id); - setDialogManager(null); }; }, [id]); - // temporarily do not render until a new dialog manager is created - if (!dialogManager) return null; - return ( {children} @@ -156,7 +170,7 @@ export const useDialogManager = ({ if (!managerInPrevState || managerInNewState?.id !== managerInPrevState.id) { setDialogManagerContext((prevState) => { - if (prevState?.dialogManager.id === managerInNewState?.id) return prevState; + if (prevState?.dialogManager === managerInNewState) return prevState; // fixme: need to handle the possibility that the dialogManager is undefined return { dialogManager: diff --git a/src/store/hooks/useStateStore.ts b/src/store/hooks/useStateStore.ts index cc7a326d40..7844a03e7d 100644 --- a/src/store/hooks/useStateStore.ts +++ b/src/store/hooks/useStateStore.ts @@ -59,7 +59,11 @@ export function useStateStore< }; }, [store, selector]); - const state = useSyncExternalStore(wrappedSubscription, wrappedSnapshot); + const state = useSyncExternalStore( + wrappedSubscription, + wrappedSnapshot, + wrappedSnapshot, + ); return state; } From ef817805f381cfcb08fe38e347f577d4a6e0f23c Mon Sep 17 00:00:00 2001 From: martincupela Date: Thu, 9 Apr 2026 16:16:19 +0200 Subject: [PATCH 2/2] test: fix DialogManagerContext.test.js import react-dom/server.node --- src/components/Dialog/__tests__/DialogManagerContext.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Dialog/__tests__/DialogManagerContext.test.js b/src/components/Dialog/__tests__/DialogManagerContext.test.js index b6b76d2022..7d18e8adac 100644 --- a/src/components/Dialog/__tests__/DialogManagerContext.test.js +++ b/src/components/Dialog/__tests__/DialogManagerContext.test.js @@ -1,6 +1,5 @@ import React from 'react'; import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; -import { renderToStaticMarkup } from 'react-dom/server'; import { DialogManagerProvider, useDialogManager, @@ -13,6 +12,8 @@ jest.mock('../../../components/Dialog/DialogPortal', () => ({ DialogPortalDestination: () => null, })); +const { renderToStaticMarkup } = require('react-dom/server.node'); + const TEST_IDS = { CLOSE_DIALOG: 'close-dialog', DIALOG_COUNT: 'dialog-count',