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>}</>
}
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>
}
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 and front end development, feel free to follow me and say hi on twitter.com/_AshConnolly! 👋 🙂
Stunning cover photo by Spencer Watson on Unsplash!
Top comments (5)
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?
Hi Lars! 👋
Look at this particular bit: joshwcomeau.com/react/the-perils-o...
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! 😄👍
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?
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:
That also worked for me pretty well. Im a big fan of abstraction and this clarifies that THIS component should only render on client.
I mean, what is the problem in checking for window? That work's perfectly.
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