From 9cf452fe6dc4073ba9272a556242c5bfc27db105 Mon Sep 17 00:00:00 2001 From: stenin-nikita Date: Wed, 10 Sep 2025 17:43:23 +0300 Subject: [PATCH] fix: defer the update in useIsSSR to avoid interrupting hydration --- packages/@react-aria/ssr/src/SSRProvider.tsx | 48 ++++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/packages/@react-aria/ssr/src/SSRProvider.tsx b/packages/@react-aria/ssr/src/SSRProvider.tsx index 83e07aa46e6..ca5a2d98fd1 100644 --- a/packages/@react-aria/ssr/src/SSRProvider.tsx +++ b/packages/@react-aria/ssr/src/SSRProvider.tsx @@ -168,11 +168,7 @@ function useModernSSRSafeId(defaultId?: string): string { export const useSSRSafeId: typeof useModernSSRSafeId | typeof useLegacySSRSafeId = typeof React['useId'] === 'function' ? useModernSSRSafeId : useLegacySSRSafeId; function getSnapshot() { - return false; -} - -function getServerSnapshot() { - return true; + return null; } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -181,17 +177,41 @@ function subscribe(onStoreChange: () => void): () => void { return () => {}; } -/** - * Returns whether the component is currently being server side rendered or - * hydrated on the client. Can be used to delay browser-specific rendering - * until after hydration. - */ -export function useIsSSR(): boolean { +function useModernIsSSR(): boolean { + let initialState = false; // In React 18, we can use useSyncExternalStore to detect if we're server rendering or hydrating. - if (typeof React['useSyncExternalStore'] === 'function') { - return React['useSyncExternalStore'](subscribe, getSnapshot, getServerSnapshot); + React.useSyncExternalStore(subscribe, getSnapshot, () => { + initialState = true; + + // Important! We must return the same value for both server and client to avoid unnecessary render + return getSnapshot(); + }); + let [isSSR, setIsSSR] = useState(initialState); + + if (typeof document !== 'undefined') { + // eslint-disable-next-line react-hooks/rules-of-hooks + useLayoutEffect(() => { + if (!isSSR) { + return; + } + + React.startTransition(() => { + setIsSSR(false); + }); + }, [isSSR]); } - // eslint-disable-next-line react-hooks/rules-of-hooks + return isSSR; +} + +function useLegacyIsSSR(): boolean { return useContext(IsSSRContext); } + +// Use React.useSyncExternalStore and React.startTransition in React >=18 if available, otherwise fall back to our old implementation. +/** + * Returns whether the component is currently being server side rendered or + * hydrated on the client. Can be used to delay browser-specific rendering + * until after hydration. + */ +export const useIsSSR: () => boolean = typeof React['startTransition'] === 'function' ? useModernIsSSR : useLegacyIsSSR;