Prevent "window is not defined" Errors With a useClientSide() Custom Hook
Sep 27, 2021 |1,995 views
TLDR:
- There is no
window
object on the server - trying to access thewindow
object will thrown an error in server side rendered code, and in Node.js based development environments - You can access
window
in auseEffect
hook, asuesEffect
only runs on the client - We want to avoid having to repeat this
useEffect
logic in every component that needs to accesswindow
- 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>}</>
}
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:
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>
}
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
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>
}
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>
}
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>}</>
}
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>
}
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! π π