Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Best strategy for checking whether (typeof window !== undefined) while using Hooks #26448

Closed
cosmicespresso opened this issue Aug 13, 2020 · 5 comments
Labels
type: question or discussion Issue discussing or asking a question about Gatsby

Comments

@cosmicespresso
Copy link

I am using a custom hook to access breakpoint information, which is using the window property, so I'm running into trouble when building.

I am calling the hook from a component like so:
const breakpoint = useBreakpoints(typeof window !== undefined)

and here is the code I have for the hook itself:

const getDeviceConfig = (width) => {
  if (width >= 320 && width < 375) {
    return 'xs';
  } else if(width >= 375 && width < 768 ) {
    return 'sm';
  } else if(width >= 768 && width <= 1024) {
    return 'md';
  } else if(width > 1024) {
    return 'lg';
  }
};

const useBreakpoint = (isBrowser) => {
  const [brkPnt, setBrkPnt] = useState(() => getDeviceConfig(isBrowser && window.innerWidth));
  
  useEffect(() => {
    const calcInnerWidth = () => {
      setBrkPnt(getDeviceConfig(isBrowser && window.innerWidth))
    }; 
    isBrowser && window.addEventListener('resize', calcInnerWidth);
    return () => isBrowser &&  window.removeEventListener('resize', calcInnerWidth);
  }, []);

  return brkPnt;
}

export default useBreakpoint

The reason I am passing the (typeof window !== undefined) value from the components to the hook is because of the "Rules of Hooks": "Don’t call Hooks inside loops, conditions, or nested functions".

This works, but results in a very weird behavior on deployed version where the page (on desktop mode) loads to the mobile layout and needs a couple of refreshes to correct itself.

Having done some reading in similar issues and answers I now understand a better way to go about it is to do something like this:

const isBrowser = typeof window !== `undefined`

// Function Component with renturned JSX only
const myComponent = (props) => (
  isBrowser && <myActualComponent data={props.data} />
)

but I want to understand exactly what is happening that triggers this weird behavior on deploy?

@cosmicespresso cosmicespresso added the type: question or discussion Issue discussing or asking a question about Gatsby label Aug 13, 2020
@gatsbot gatsbot bot added the status: triage needed Issue or pull request that need to be triaged and assigned to a reviewer label Aug 13, 2020
@polarathene
Copy link
Contributor

(typeof window !== undefined) is always going to be the same value from the first time it's encountered(either during SSR or on the client), it's a global, so no need to pass it in as a value, just include it with your hook where it's used out of it's scope and reference it as you do with isBrowser, gatsby-image does this.

Having done some reading in similar issues and answers I now understand a better way to go about it is to do something like this

Yes, as I showed in that snippet, but if it only matters for your hook as a condition, you don't need to wrap it like my example snippet shows there. That was for third-party imported components that were causing problems because they internally use window and don't have support for SSR otherwise.

This works, but results in a very weird behavior on deployed version where the page (on desktop mode) loads to the mobile layout and needs a couple of refreshes to correct itself.

SSR will happen with a minimal viewport afaik, so expect anything that might bake in inline CSS or markup/components based on width to favor mobile breakpoints.

You can also use the window.matchMedia API for a listener that does similar to what you're doing here (I have an active PR doing such with gatsby-image, but not hooks based), there's also a package react-media I think that achieves similar if that suits you, can't recall but it might also have a hook.

For the lack of updating/responsiveness in the situation you describe, it can be due to hydration. React hydrates from the SSR html it first loads, and assumes the state it would compute would match the HTML it received, and doesn't bother to check for a mismatch, so you need to trigger a re-render that would alter the returned JSX if I recall.. I have a basic PR here for gatsby-image to handle a similar issue.

If you instead get a momentary flicker, that'd be from the SSR output before React/JS kicks in, and may require additional handling, I have a PR showing that fix off too here.

@cosmicespresso
Copy link
Author

Thanks for taking the time to write all this, its very helpful.

I have changed my code to not use the hook and use gatsby-plugin-breakpoints instead (link) and now I get this momentary flicker on initial load, but it only happens then and the rest of the navigation seems to be fine.

Do you have a sense of when they might approve the PR? It looks like all checks are fine.

@polarathene
Copy link
Contributor

now I get this momentary flicker on initial load, but it only happens then and the rest of the navigation seems to be fine.

The flicker is probably the original HTML/CSS rendered from SSR, prior to React taking over. If it persists until changing state in React such as navigation or window resize event, then it may also be a hydration issue with the component.

Do you have a sense of when they might approve the PR? It looks like all checks are fine.

Whenever they decide to review and approve my PRs, some are partly almost there but waiting on some feedback, slow process unfortunately.


I have changed my code to not use the hook and use gatsby-plugin-breakpoints instead

Presumably, that's conditionally changing the rendered component based on the breakpoint chosen. So during SSR that would be fixed with the smallest breakpoint I think, whereas you may want something more dynamic.

If it's working fine once the page is fully loaded and React has done it's thing, then it's fixable with CSS media queries. You can add those with <style> element in the component, or via a CSS-in-JS lib or separate CSS that you add in your project somewhere to provide those media queries.

It's not likely compatible with how that plugin is working, as if the markup is changing, not just CSS, you'll probably want to render all variants instead and use media queries to toggle visibility.

react-media allows for specifying the SSR/default breakpoint, but if you want to handle it prior to React/JS fixing it, to avoid the flicker issue, then you probably do need to go with the visibility toggle, at least for SSR until a client version can be used.

Another alternative is to hide the component at SSR(when window isn't available, like shown in the snippet of mine you quoted), it should minimize the issue, or you can use some CSS transition to fade-in/reveal the component.

@cosmicespresso
Copy link
Author

cosmicespresso commented Aug 15, 2020

I "fixed" (went around) my problem by reading and following the suggestions in this great article: https://joshwcomeau.com/react/the-perils-of-rehydration/ (wrapping everything in a ClientOnly wrapper).

@polarathene
Copy link
Contributor

That's a bit odd, since that's a hydration issue not an SSR one where isBrowser guard would work better. Good that it resolved your issue though.

@LekoArts LekoArts removed the status: triage needed Issue or pull request that need to be triaged and assigned to a reviewer label Aug 25, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: question or discussion Issue discussing or asking a question about Gatsby
Projects
None yet
Development

No branches or pull requests

3 participants