adobe / react-spectrum

A collection of libraries and tools that help you build adaptive, accessible, and robust user experiences.
https://react-spectrum.adobe.com
Apache License 2.0
13.03k stars 1.13k forks source link

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

Closed josharens closed 1 year ago

josharens commented 2 years ago

πŸ› 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/ssr@3.2.0
Browser Chrome
Operating System MacOS

🧒 Your Company/Team

SmugMug

devongovett commented 2 years ago

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.

josharens commented 2 years ago

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?

devongovett commented 2 years ago

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...

josharens commented 2 years ago

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.

devongovett commented 2 years ago

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

josharens commented 2 years ago

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.