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.tsx b/src/components/Dialog/__tests__/DialogManagerContext.test.tsx index 66edd0f875..ca0ec9d61f 100644 --- a/src/components/Dialog/__tests__/DialogManagerContext.test.tsx +++ b/src/components/Dialog/__tests__/DialogManagerContext.test.tsx @@ -1,5 +1,7 @@ import React from 'react'; import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { vi } from 'vitest'; import { DialogManagerProvider, useDialogManager, @@ -7,6 +9,10 @@ import { import { useDialogIsOpen, useOpenedDialogCount } from '../hooks'; +vi.mock('../../../components/Dialog/service/DialogPortal', () => ({ + DialogPortalDestination: () => null, +})); + const TEST_IDS = { CLOSE_DIALOG: 'close-dialog', DIALOG_COUNT: 'dialog-count', @@ -89,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 74a49fabc3..1f015d1971 100644 --- a/src/context/DialogManagerContext.tsx +++ b/src/context/DialogManagerContext.tsx @@ -15,27 +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 = ({ - closeOnClickOutside, - id, -}: { - closeOnClickOutside?: boolean; - id: string; -}) => { - let manager = getDialogManager(id); - if (!manager) { - manager = new DialogManager({ closeOnClickOutside, id }); - dialogManagersRegistry.partialNext({ [id]: manager }); - } else if (typeof closeOnClickOutside === 'boolean') { - manager.closeOnClickOutside = closeOnClickOutside; - } - return manager; -}; - const removeDialogManager = (id: string) => { if (!getDialogManager(id)) return; dialogManagersRegistry.partialNext({ [id]: undefined }); @@ -67,23 +52,50 @@ export const DialogManagerProvider = ({ closeOnClickOutside, id, }: DialogManagerProviderProps) => { - const [dialogManager, setDialogManager] = useState(() => { - if (id) return getDialogManager(id) ?? null; + const [dialogManager, setDialogManager] = useState(() => { + if (id) { + const manager = + getDialogManager(id) ?? + pendingDialogManagersById[id] ?? + new DialogManager({ closeOnClickOutside, id }); + + pendingDialogManagersById[id] = manager; + return manager; + } return new DialogManager({ closeOnClickOutside }); // will not be included in the registry }); useEffect(() => { if (!id) return; - setDialogManager(getOrCreateDialogManager({ closeOnClickOutside, id })); + const manager = + getDialogManager(id) ?? + pendingDialogManagersById[id] ?? + new DialogManager({ closeOnClickOutside, id }); + + if (typeof closeOnClickOutside === 'boolean') { + manager.closeOnClickOutside = closeOnClickOutside; + } + + 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); }; }, [closeOnClickOutside, id]); - // temporarily do not render until a new dialog manager is created - if (!dialogManager) return null; - return ( {children} @@ -172,7 +184,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; }