Re-rendering React Components on Window Resizes With a Threshold
Tags: react, webdev, tutorial, javascript
Published: 2023-05-01 23:32:05

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:

```javascript
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 /> }
    </>
  )
}
```

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 <em>while</em> 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: 

```javascript

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

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:

```javascript
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
```

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

```javascript
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 /> }
    </>
  )
}
```

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!