react-spectrum icon indicating copy to clipboard operation
react-spectrum copied to clipboard

`useIsSSR()` hook always returns `true` and then `false` after hydration on mount

Open josharens opened this issue 3 years ago โ€ข 6 comments

๐Ÿ› Bug Report

I may have misunderstood the useIsSSR() documentation, but it isn't working the way I would expect it to.

๐Ÿค” Expected Behavior

The useIsSSR() hook should always return false after the app has hydrated on the client.

๐Ÿ˜ฏ Current Behavior

Currently, the hook always returns true (meaning the app is server side rendering, or hydrating) before flipping to false every time it's mounted, even after hydration. I would expect it to always return false after hydration instead of true and then false.

๐Ÿ’ Possible Solution

No possible solution, but this line seems suspect to me. The isSSR state is always initially true, even if the app has already hydrated.

๐Ÿ”ฆ Context

My use case involves using useIsSSR() to create a hook that returns the device's pixel ratio. On the server and during hydration this hook returns a default value. After hydration it should always return window.devicePixelRatio.

import { useIsSSR } from '@react-aria/ssr';

function useDevicePixelRatio(defaultRatio = 1) {
  let isSSROrHydration = useIsSSR();

  return isSSROrHydration
    ? defaultRatio
    : window.devicePixelRatio;
}

๐Ÿ’ป Code Sample

This example is a bit contrived since there isn't any server side rendering happening, but it still illustrates the potential issue.

https://codesandbox.io/s/tender-violet-kunchu?file=/src/App.js

๐ŸŒ Your Environment

Software Version(s)
react-spectrum @react-aria/[email protected]
Browser Chrome
Operating System MacOS

๐Ÿงข Your Company/Team

SmugMug

josharens avatar Jul 11 '22 17:07 josharens

This is intentional. It's required to avoid hydration mismatches between what's rendered on the server and client, or React will emit warnings. So the first render will always match what was rendered on the server, and then update to the client rendering afterward.

devongovett avatar Jul 11 '22 19:07 devongovett

Hey @devongovett, thanks for responding. So just to be clear, if my app has already hydrated, and then I render a new component using useIsSSR(), it's expected that it should first return true and then false, instead of just returning false?

josharens avatar Jul 11 '22 19:07 josharens

Ah I understand what you mean. We could potentially keep track of that I guess using some global flag, but not exactly sure how we'd know when hydration is complete for all components...

devongovett avatar Jul 11 '22 19:07 devongovett

not exactly sure how we'd know when hydration is complete for all components...

Previously I had a useEffect() in my root <App> component that would update a HydrationContext when run, signaling that hydration had completed. This may be naive, but theoretically something similar could be done in SSRProvider.

I'm on React 17 currently, so I haven't put any thought into whether or not React 18 throws a wrench in that.

josharens avatar Jul 11 '22 19:07 josharens

Yeah I think partial hydration/async rendering probably would make this harder.

devongovett avatar Jul 11 '22 20:07 devongovett

Doing a little more digging into React 18's selective hydration, and I think for my current specific use case, using the new useSyncExternalStore hook (or the shim since I'm on React 17 at the moment) is a better solution than trying to track when hydration is complete myself.

I still think that the useIsSSR() hook seems a little flawed, but I don't have a good solution. Perhaps the useSyncExternalStore() hook will ultimately end up becoming the go-to for this sort of problem anyways.

josharens avatar Jul 13 '22 17:07 josharens