DEV Community

Cover image for Prevent "window is not defined" Errors With a useClientSide() Custom Hook
Ash Connolly
Ash Connolly

Posted on • Updated on • Originally published at ashconnolly.com

Prevent "window is not defined" Errors With a useClientSide() Custom Hook

TLDR:

  • There is no window object on the server - trying to access the window object will thrown an error in server side rendered code, and in Node.js based development environments
  • You can access window in a useEffect hook, as uesEffect only runs on the client
  • We want to avoid having to repeat this useEffect logic in every component that needs to access window
  • Instead we can move this logic into a custom react hook to keep everything super tidy! πŸŽ‰

The finished useClientSide() hook:

const useClientSide = func => {
  const [value, setValue] = useState(null)
  useEffect(() => {
    setValue(func())
  }, [func])
  return value
}

const getUserAgent = () => window.navigator.userAgent

export default function Example() {
  const userAgent = useClientSide(getUserAgent)
  return <>{userAgent && <p>{userAgent}</p>}</>
}
Enter fullscreen mode Exit fullscreen mode

Heres a stackblitz⚑ Next.js demo.

The Problem

When trying to access window with react frameworks like Next.js you might run into issues when trying to access the window object and see the following error:

window is not defined error

This is because somewhere in your app window is trying to be accessed from the server, where it does not exist.

In Next.js this could be because we're trying to access window on a page that uses getServerSideProps, which makes the page a server side rendered (SSR).

You might think:

"But my app is static, with no server side rendering, why am I getting Server errors??"

Most development environments are created by running a local Node.js server (Next.js does this). And as Node.js runs on the server, there is no window object

Example Problem: Device Detection

Say if you had a button, and on a touch device you want it to say "Tap here", otherwise it would say "Click here", you could check the window object for navigator.userAgent.

This would tell us what device type they're on, like Android or IOS, and we could infer if it's a touch device. There are other ways to check touch devices, but for the purposes of this tutorial, we'll do it this way.

You could approach it like this for client side rendered apps:

const isTouchDevice = () => {
  const ua = window.navigator.userAgent
  if (ua.match(/Android|webOS|iPhone|iPad|iPod|BlackBerry|BB10|PlayBook|IEMobile|Opera Mini/i)) {
    return true
  }
  return false
}

export default function Example() {
  const isTouch = isTouchDevice()
  return <button>{isTouch ? 'Tap here!' : 'Click here!'}</button>
}
Enter fullscreen mode Exit fullscreen mode

Note: I won't show the code for isTouchDevice() again, just to keep the code examples clearer. Just remember it returns true or false! :)

Here we are getting the window.navigator.userAgent and then passing it into our function and checking if it contains any identifiers for touch devices, if it does return true, otherwise return false.

However, this code will cause the window is not defined error, as our local dev environment is running on a server, where there is no window object!

A Common, But Not Ideal Solution πŸ™…β€β™‚οΈ

We could check if window is not defined by adding this line at the top of any function that tries to access window:

  if (typeof window === 'undefined') return 
Enter fullscreen mode Exit fullscreen mode

Note you cannot do window === undefined as this assume would window is declared, but has no value. When actually, window hasn't been declared at all. This is the difference between:

  • undefined: a variable that is declared but not initialised or defined (aka not given a value)
  • not defined: a variable that has not been declared at all

Using typeof window === 'undefined' is far from ideal and can cause rendering issues as explained in this brilliant in-depth article by @joshwcomeau: The Perils Of Rehydration.

The Solution: Only Reference Window On The Client πŸ‘

We can do this by running our isTouchDevice() function inside a useEffect, which only runs on the client when the component mounts.

We can also store the return value of isTouchDevice() in state by using useState. Storing it in state means that it's value is preserved during re-renders.

Here's a working example:

import { useEffect, useState } from 'react'

const isTouchDevice = () => {} // returns true or false, see code above

export default function Example() {
  const [isTouch, setisTouch] = useState(null)

  useEffect(() => {
    setisTouch(isTouchDevice())
  }, [])

  return <button>{isTouch ? 'Tap here!' : 'Click here!'}</button>
}
Enter fullscreen mode Exit fullscreen mode

Once the component mounts (which only happens on the client) the function is run and the state of isTouch is updated to a true or false value, which causes our button to show the correct messaging.

πŸ€” But having to do this every time you want to use the isTouchDevice function is really a hassle and will lead to lots of needless repetition of useEffect().

What would be much neater is a custom react hook that obfuscates all of this logic, allowing us to do something like this:

export default function Example() {
  const isTouch = useIsTouchDevice()
  return <p>{isTouch ? 'Tap here!' : 'Click here!'}</p>
}
Enter fullscreen mode Exit fullscreen mode

That would help make things easier, but something else would be better...

A Step Further: Making a useClientSide() Hook! πŸ”₯

What would be even better than a useIsTouchDevice() hook? A flexible, generalized custom hook that could take any function as an argument, and only run that function on the client side: a useClientSide() hook! πŸ˜ƒ

Example:

const useClientSide = func => {
  const [value, setValue] = useState(null)
  useEffect(() => {
    setValue(func())
  }, [func])
  return value
}

const getUserAgent = () => window.navigator.userAgent

export default function Example() {
  const userAgent = useClientSide(getUserAgent)
  return <>{userAgent && <p>{userAgent}</p>}</>
}
Enter fullscreen mode Exit fullscreen mode

What this custom hook is doing:

  • taking a function as an argument
  • calling that function in a useEffect hook (which is only done on the client)
  • saving what is returned by that function to the local state of the useClientSide() hook
  • then returning that local state value

Now let's use it with our isTouchDevice() function:

import { useEffect, useState } from 'react'

const isTouchDevice = () => {
  const ua = window.navigator.userAgent
  if (ua.match(/Android|webOS|iPhone|iPad|iPod|BlackBerry|BB10|PlayBook|IEMobile|Opera Mini/i)) {
    return true
  }
  return false
}

const useClientSide = func => {
  const [value, setValue] = useState(null)
  useEffect(() => {
    setValue(func())
  }, [func])
  return value
}

export default function Example() {
  const isTouch = useClientSide(isTouchDevice)
  return <p>{isTouch ? 'Tap here!' : 'Click here!'}</p>
}
Enter fullscreen mode Exit fullscreen mode

Heres a stackblitz⚑ Next.js demo.

If you want to check the isTouch is working as expected, just simulate a mobile device using your browser's dev tools. Like device mode in chrome.

Done!

There we go! All working! We have a useful, reusable custom hook that allows use to run any client specific code easily! πŸ˜ƒ πŸŽ‰

I built this hook while building episoderatings.com (a way to view episode ratings in a graph), to help me easily detect touch devices and display specific messaging!

episode ratings

If you like React, Next.js and front end development, feel free to follow me and say hi on twitter.com/_AshConnolly! πŸ‘‹ πŸ™‚

Stunning cover photo by Spencer Watson on Unsplash!

Oldest comments (5)

Collapse
 
_genjudev profile image
Larson

So what is the problem with checking if window exist?

I don't see the problem even after reading:
joshwcomeau.com/react/the-perils-o...

Am I missing something?

Collapse
 
ashconnolly profile image
Ash Connolly

Hi Lars! πŸ‘‹
Look at this particular bit: joshwcomeau.com/react/the-perils-o...

By rendering something different depending on whether we're within the server-side render or not, we're hacking the system. We're rendering one thing on the server, but then telling React to expect something else on the client.

Somewhat remarkably, React can still handle this situation sometimes. You may have done this yourself, and gotten away with it. But you're playing with fire. The rehydration process is optimized to be ⚑️ fast ⚑️, not to catch and fix mismatches."

That last paragraph helped me understand it.

Sure you're doing something that does achieve the goal and works, but it's something React doesn't expect you to do. React tells you this is wrong by giving you an error that says the client DOM didn't match the server.

If React is giving us an error, we should listen as it could cause serious bugs to pop up in your app, perhaps when React updates or adds some new feature that relies on this error being absent.

Hopefully that helps! πŸ˜„πŸ‘

Collapse
 
_genjudev profile image
Larson • Edited

Hi Ash,
Thanks for the answer. This helped me a little for now.

what would happen if we don't return null and just use window related stuff in scope?

if (typeof window !== 'undefined') {
    // access window here
  }
Enter fullscreen mode Exit fullscreen mode

I'm not quite sure how hydration is working in detail. I guess that would be clarify a lot for me. Todo for the next days :P

Also an option would be to dynamic import components with client related stuff:

// nextjs - https://nextjs.org/docs/advanced-features/dynamic-import
const DynamicComponentWithNoSSR = dynamic(
  () => import('../components/myComponent'),
  { ssr: false } // disable ssr here
)
Enter fullscreen mode Exit fullscreen mode

That also worked for me pretty well. Im a big fan of abstraction and this clarifies that THIS component should only render on client.

Collapse
 
_genjudev profile image
Larson

I mean, what is the problem in checking for window? That work's perfectly.

Thread Thread
 
fkrasnowski profile image
Franciszek Krasnowski • Edited

No, It’s not. The initial state of the component has to be identical to server-side output. That’s why the author is using useState and sets the state after its first render