How to Listen to Events on the Window Object with Next.JS

When trying to access the Window object from a functional component in NextJS, you will stumble upon an error. The error will let you know that the Window object is undefined.

You might think that adding 'use client' at the beginning of the file will solve the issue, but it doesn’t. So then, how do we use the Window object in Next.JS?

In order to use the Window object for any purpose, we need to wrap the usage within the useEffect hook. Because useEffect will only run on the client side.

let’s develop an example to understand this more in-depth.

You can find the source code on Github.

Do not use ‘use client’

The 'use client' directive will mark the component as client-only, but even so, the component might be actually rendered on the server side and used on the client side for hydration.

Furthermore, we don’t want to mark the whole component as client-only when we only need a certain feature to run on the client side!

Adding Event Listeners to the Window

Let’s explore a small example. We want to listen to a resize event and perform some logic. Let’s say then that we have this page called color-box.tsx. It will be a full-screen box that will change color depending on the window size.

import { CSSProperties } from 'react'

const ColorBox = () => {
  const LIGHT_COLOR = '#dfdfdf'
  const DARK_COLOR = '#424241'

  const style: CSSProperties = {
    backgroundColor: LIGHT_COLOR,
    width: '100%',
    height: '100vh',
  }

  return <div style={style} />
}

export default ColorBox

To change the background color we need to add an event listener on the window object and handle the resize event, and of course, make it a local state.

import { CSSProperties, useEffect, useState } from 'react'

const ColorBox = () => {
  const LIGHT_COLOR = '#dfdfdf'
  const DARK_COLOR = '#424241'

  // using light color by default
  const [backgroundColor, setBackgroundColor] = useState(LIGHT_COLOR)
  
  useEffect(() => {
    const COLOR_CHANGE_BREAKPOINT_IN_PX = 800

    const onResize = () => {
      if (window.innerWidth > COLOR_CHANGE_BREAKPOINT_IN_PX) {
        setBackgroundColor(LIGHT_COLOR)
      } else {
        setBackgroundColor(DARK_COLOR)
      }
    }

    window.addEventListener('resize', onResize)
  }, []) // no dependencies

  const baseStyle: CSSProperties = {
    width: '100%',
    height: '100vh',
  }

  return <div style={{
    ...baseStyle,
    backgroundColor
  }} />
}

export default ColorBox

As you can see, we declare the handler also inside the effect callback.

Now if we try it by starting our server and going to http://localhost:3000/color-box we can see that the background color changes when we go below 800 pixels in width.

Next, if we are below 800 pixels and refresh the page, we will see the wrong background color, that’s because the default value is the light color. Let’s fix that!

First, lift the constant COLOR_CHANGE_BREAKPOINT_IN_PX up from the effect to the component scope, since we will use it in two effects now, the one we had and a new one that we will write. Then add the new effect that will set the background color without using any events.

import { CSSProperties, useEffect, useState } from 'react'

const ColorBox = () => {
  const COLOR_CHANGE_BREAKPOINT_IN_PX = 800
  const LIGHT_COLOR = '#dfdfdf'
  const DARK_COLOR = '#424241'

  const [backgroundColor, setBackgroundColor] = useState(LIGHT_COLOR)

  useEffect(() => {
    const onResize = () => {
      if (window.innerWidth > COLOR_CHANGE_BREAKPOINT_IN_PX) {
        setBackgroundColor(LIGHT_COLOR)
      } else {
        setBackgroundColor(DARK_COLOR)
      }
    }

    window.addEventListener('resize', onResize)
  }, [])
  
  // set the correct color by default
  useEffect(() => {
    if (window.innerWidth > COLOR_CHANGE_BREAKPOINT_IN_PX) {
      setBackgroundColor(LIGHT_COLOR)
    } else {
      setBackgroundColor(DARK_COLOR)
    }
  }, [])

  const baseStyle: CSSProperties = {
    width: '100%',
    height: '100vh',
  }

  return <div style={{
    ...baseStyle,
    backgroundColor
  }} />
}

export default ColorBox

Finally, since we have now duplicate logic, we can refactor it into a function.

import { CSSProperties, useEffect, useState } from 'react'

const ColorBox = () => {
  const COLOR_CHANGE_BREAKPOINT_IN_PX = 800
  const LIGHT_COLOR = '#dfdfdf'
  const DARK_COLOR = '#424241'

  const [backgroundColor, setBackgroundColor] = useState(LIGHT_COLOR)

  const changeColor = () => {
    if (window.innerWidth > COLOR_CHANGE_BREAKPOINT_IN_PX) {
      setBackgroundColor(LIGHT_COLOR)
    } else {
      setBackgroundColor(DARK_COLOR)
    }
  }

  useEffect(() => {
    const onResize = () => changeColor()

    window.addEventListener('resize', onResize)
  }, [])

  useEffect(() => {
    changeColor()
  }, [])

  const baseStyle: CSSProperties = {
    width: '100%',
    height: '100vh',
  }

  return <div style={{
    ...baseStyle,
    backgroundColor
  }} />
}

export default ColorBox

Removing Event Listeners from the Window

It is a good practice to always remove event listeners when we don’t need them to prevent memory leaks. Hence, in our first effect, we can return a method that does just that. when the component has been destroyed the method we return will be executed by React.


import { CSSProperties, useEffect, useState } from 'react'

const ColorBox = () => {
  const COLOR_CHANGE_BREAKPOINT_IN_PX = 800
  const LIGHT_COLOR = '#dfdfdf'
  const DARK_COLOR = '#424241'

  const [backgroundColor, setBackgroundColor] = useState(LIGHT_COLOR)

  const changeColor = () => {
    if (window.innerWidth > COLOR_CHANGE_BREAKPOINT_IN_PX) {
      setBackgroundColor(LIGHT_COLOR)
    } else {
      setBackgroundColor(DARK_COLOR)
    }
  }

  useEffect(() => {
    const onResize = () => changeColor()

    window.addEventListener('resize', onResize)

    return () => {
      window.removeEventListener('resize', onResize)
    }
  }, [])

  useEffect(() => {
    changeColor()
  }, [])

  const baseStyle: CSSProperties = {
    width: '100%',
    height: '100vh',
  }

  return <div style={{
    ...baseStyle,
    backgroundColor
  }} />
}

export default ColorBox

Accessing the Document Object

The second and last example would be manipulating the DOM. In this example, we will change the background color of the body tag on a page to a dark color and revert it to a light one when we navigate away.

Using the same pattern we will wrap our logic within useEffect.

First, create a new page under the pages directory, and call it dark.tsx, then export a functional component and declare a new effect:

import { useEffect } from 'react'

const Dark = () => {
  useEffect(() => {
    const LIGHT_COLOR = '#dfdfdf'
    const DARK_COLOR = '#424241'

    document.body.style.backgroundColor = LIGHT_COLOR
  }, [])

  return <div>This page should have a dark background</div>
}

export default Dark

Next, remove this dark color. Return an anonymous function from the useEffect callback. It will be executed when the component is destroyed:

import { useEffect } from 'react'

const LoginPage = () => {
  useEffect(() => {
    const LIGHT_COLOR = '#dfdfdf'
    const DARK_COLOR = '#424241'

    document.body.style.backgroundColor = DARK_COLOR

    return () => {
      document.body.style.backgroundColor = LIGHT_COLOR
    }
  })

  return <div>This page should have a dark background</div>
}

export default LoginPage

To test this add navigation. in this page add a Next Link that navigates to the home page.

import { useEffect } from 'react'
import Link from 'next/link';

const Dark = () => {
  useEffect(() => {
    const LIGHT_COLOR = '#dfdfdf'
    const DARK_COLOR = '#424241'

    document.body.style.backgroundColor = DARK_COLOR

    return () => {
      document.body.style.backgroundColor = LIGHT_COLOR
    }
  }, [])

  return <div style={{color: 'wheat'}}>
      This page should have a dark background
      <br />
      <Link href="/">Go to back home!</Link>
    </div>
}

export default Dark

And in the home page component add a button that navigates to this page.

import Link from 'next/link';

const Home = () => {
  return (
    <>
      Home page
      <br />
      <Link href="/dark">Go to dark page!</Link>
    </>
  )
}

export default Home

We can test it now by going to https://localhost:3000 and changing routes

Conclusion

We have explored a few examples of how to write clean and maintainable React components that utilize the window and document objects through the use of useEffect I hope you have learned something valuable.

If you have any suggestions or improvements to this approach then please leave me a comment, cheers!

Amenallah Hsoumi
Amenallah Hsoumi

Senior Software Engineer, Indie Hacker, building cool stuff on the web!

Articles: 19