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

Sep 27, 2021 Loading...

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>}</>
}

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

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

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

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

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>}</>
}

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

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!

If you like React, Next.js or front-end development in general, feel free to follow and say hi on Twitter @_AshConnolly! ๐Ÿ‘‹ ๐Ÿ™‚