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 /> }
</>
)
}
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 /> }
</>
)
}
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
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 /> }
</>
)
}
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)
Probably
matchMedia
can also fit your requirement with flexible and maintainable implementation.developer.mozilla.org/en-US/docs/W...
With matchMedia I got this result: