DEV Community

Cover image for Re-rendering React Components on Window Resizes With a Threshold
Thomas Musselman
Thomas Musselman

Posted on

Re-rendering React Components on Window Resizes With a Threshold

While building a mobile friendly React app, I found myself wanting to accomplish something out of the scope of CSS media queries. I wanted to render one of two separate components conditionally depending on the width of the window. I also wanted the component to change if the browser was resized. This was the tricky part.

After doing some research I implemented a couple of solutions which worked perfectly fine, but I was overall unhappy with the result. Most took an approach similar to the following:

const MAX_MOBILE_WIDTH = 600

const App = () => {
  const [width, setWidth] = useState(window.innerWidth)

  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth)
    window.addEventListener("resize", handleResize)
    return () => window.removeEventListener("resize", handleResize)
  }, [])

  const isMobile = width <= MOBILE_WIDTH

  return (
    <>
      { isMobile ? <MobileComponent /> : <FullSizeComponent /> }
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

The problem with this approach is that it causes a re-render each every time the window is resized, even if mobile threshold is never crossed. It also re-renders the page while the page is being resized for each pixel in size change! This may not be a huge problem but definitely isn't the most efficient way to accomplish this.

The only solutions I found to this efficiency problem is to "debounce" the resize event listener function so it doesn't fire quite as often. This solutions works great, but still causes re-renders when unnecessary, as well as causing a delay on the re-render once a resize is complete.

Finding a Solution

After some tinkering I came up a solution which utilizes the useRef hook to keep track of the previous window size in the event listener and only triggers a re-render when the current window size and previous window size are on different sides of the threshold of 600px. Here's the code:


const MAX_MOBILE_WIDTH = 600

const App = () => {
  const [isMobile, setIsMobile] = useState(window.innerWidth <= MOBILE_WIDTH)
  const prevWidth = useRef(window.innerWidth)

  useEffect(() => {
    const handleResize = () => {
      const currWidth = window.innerWidth
      if (currWidth <= MOBILE_WIDTH && prevWidth.current > MOBILE_WIDTH){
        setIsMobile(true)
      } else if (currWidth > MOBILE_WIDTH && prevWidth.current <= MOBILE_WIDTH) {
        setIsMobile(false)
      }
      prevWidth.current = currWidth
    }
    window.addEventListener("resize", handleResize)
    return () => window.removeEventListener("resize", handleResize)
  }, [])

  return (
    <>
      { isMobile ? <MobileComponent /> : <FullSizeComponent /> }
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

While it's a little more code it drastically cuts down on the number of re-renders and only triggers one when necessary. It also makes it easy to use a useEffect hook on isMobile and trigger any additional code when the breakpoint is reached. It could also be combined with an event listener "debouncer" to make it even more efficient.

Abstracting to a Custom Hook

Finally I turned this into a custom hook so it can easily be used anywhere in the application:

import { useEffect, useRef, useState } from "react"

const useWindowResizeThreshold = threshold => {
  const [isMobileSize, setIsMobileSize] = useState(window.innerWidth <= threshold)
  const prevWidth = useRef(window.innerWidth)

  useEffect(() => {
    const handleResize = () => {
      const currWidth = window.innerWidth
      if (currWidth <= threshold && prevWidth.current > threshold){
        setIsMobileSize(true)
      } else if (currWidth > threshold && prevWidth.current <= threshold) {
        setIsMobileSize(false)
      }
      prevWidth.current = currWidth
    }

    window.addEventListener("resize", handleResize)
    return () => window.removeEventListener("resize", handleResize)
  }, [])

  return isMobileSize
}

export default useWindowResizeThreshold
Enter fullscreen mode Exit fullscreen mode

This allowed me to easily set up my component and another useEffect like so:

const MAX_MOBILE_WIDTH = 600

const App = () => {
  const isMobileSize = useWindowResizeThreshold(MAX_MOBILE_WIDTH)

  useEffect(() => {
    //Some more code to execute when the mobile size is toggled
  }, [isMobileSize])

  return (
    <>
      { isMobileSize ? <MobileComponent /> : <FullSizeComponent /> }
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

I ultimately decided not to use a debounce function as the complexity of the code running in my handleResize function is fairly minimal and I appreciate the responsiveness of having a re-render while resizing.

I'm new to React so please chip in with any thoughts, guidance, or feedback!

Top comments (2)

Collapse
 
ronyu profile image
Ron Yu

Probably matchMedia can also fit your requirement with flexible and maintainable implementation.

developer.mozilla.org/en-US/docs/W...

Collapse
 
lucasspeixoto profile image
Lucas Peixoto

With matchMedia I got this result:

import { useEffect, useState } from 'react';

const useWindowDimensions = (innerWidth: number) => {
  const [isMobileSize, setIsMobileSize] = useState(window.innerWidth <= innerWidth);

  useEffect(() => {
    const windowResizeHandler = () => {
      const matchMediaString = `(max-width: ${innerWidth}px)`;

      if (matchMedia(matchMediaString).matches) {
        setIsMobileSize(true);
      } else {
        setIsMobileSize(false);
      }
    };

    window.addEventListener('resize', windowResizeHandler);
    return () => window.removeEventListener('resize', windowResizeHandler);
  }, []);

  return isMobileSize;
};

export default useWindowDimensions;
Enter fullscreen mode Exit fullscreen mode