Skip to content

Commit 9cf452f

Browse files
committed
fix: defer the update in useIsSSR to avoid interrupting hydration
1 parent 661ba8e commit 9cf452f

File tree

1 file changed

+34
-14
lines changed

1 file changed

+34
-14
lines changed

packages/@react-aria/ssr/src/SSRProvider.tsx

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -168,11 +168,7 @@ function useModernSSRSafeId(defaultId?: string): string {
168168
export const useSSRSafeId: typeof useModernSSRSafeId | typeof useLegacySSRSafeId = typeof React['useId'] === 'function' ? useModernSSRSafeId : useLegacySSRSafeId;
169169

170170
function getSnapshot() {
171-
return false;
172-
}
173-
174-
function getServerSnapshot() {
175-
return true;
171+
return null;
176172
}
177173

178174
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -181,17 +177,41 @@ function subscribe(onStoreChange: () => void): () => void {
181177
return () => {};
182178
}
183179

184-
/**
185-
* Returns whether the component is currently being server side rendered or
186-
* hydrated on the client. Can be used to delay browser-specific rendering
187-
* until after hydration.
188-
*/
189-
export function useIsSSR(): boolean {
180+
function useModernIsSSR(): boolean {
181+
let initialState = false;
190182
// In React 18, we can use useSyncExternalStore to detect if we're server rendering or hydrating.
191-
if (typeof React['useSyncExternalStore'] === 'function') {
192-
return React['useSyncExternalStore'](subscribe, getSnapshot, getServerSnapshot);
183+
React.useSyncExternalStore(subscribe, getSnapshot, () => {
184+
initialState = true;
185+
186+
// Important! We must return the same value for both server and client to avoid unnecessary render
187+
return getSnapshot();
188+
});
189+
let [isSSR, setIsSSR] = useState(initialState);
190+
191+
if (typeof document !== 'undefined') {
192+
// eslint-disable-next-line react-hooks/rules-of-hooks
193+
useLayoutEffect(() => {
194+
if (!isSSR) {
195+
return;
196+
}
197+
198+
React.startTransition(() => {
199+
setIsSSR(false);
200+
});
201+
}, [isSSR]);
193202
}
194203

195-
// eslint-disable-next-line react-hooks/rules-of-hooks
204+
return isSSR;
205+
}
206+
207+
function useLegacyIsSSR(): boolean {
196208
return useContext(IsSSRContext);
197209
}
210+
211+
// Use React.useSyncExternalStore and React.startTransition in React >=18 if available, otherwise fall back to our old implementation.
212+
/**
213+
* Returns whether the component is currently being server side rendered or
214+
* hydrated on the client. Can be used to delay browser-specific rendering
215+
* until after hydration.
216+
*/
217+
export const useIsSSR: () => boolean = typeof React['startTransition'] === 'function' ? useModernIsSSR : useLegacyIsSSR;

0 commit comments

Comments
 (0)