React Responsive
How do you make your React applications responsive for any sized device? Let's see how to do so by making our own custom React hook
At the top of my React site is a Header component. As I decrease the size of the page, I want to show fewer links:
To do this we could use a media query with CSS, or we could use a custom React hook to give us the current size of the page and hide or show the links in our JSX.
Previously, I was using a hook from the a library called react-use to add this functionality.
Instead of bringing an entire third-party library, however, I decided to create my own hook that would provide the dimensions of the window, both the width and height. I called this hook useWindowSize.
How to create the custom hook
First, we’ll create a new file .js in our utilities (utils) folder, the same name as the hook useWindowSize
and I’ll import React (to use hooks) while exporting the custom hook.
// utils/useWindowSize.js
import React from "react";
export default function useWindowSize() {}
Now since I’m using this within a Gatsby site, which is server rendered, I need to get the size of the window. But we may not have access to it because we’re on the server.
To check and make sure we’re not on the server, we can see if type of window is not equal to the string undefined.
In which case we can return to a default width and height for a browser, say, 1200 and 800 within an object:
// utils/useWindowSize.js
import React from "react";
export default function useWindowSize() {
if (typeof window !== "undefined") {
return { width: 1200, height: 800 };
}
}
How to get the width and height from window
And assuming we are on the client and can get the window, we can take the useEffect hook to perform a side effect by interacting with window. We’ll include an empty dependencies array to make sure the effect function is called only once the component (that this hook is called in) is mounted.
To find out the window width and height, we can add an event listener and listen for the resize event. And whenever the browser sizes change, we can update a piece of state (created with useState), which we’ll call windowSize and the setter to update it will be setWindowSize.
// utils/useWindowSize.js
import React from "react";
export default function useWindowSize() {
if (typeof window !== "undefined") {
return { width: 1200, height: 800 };
}
const [windowSize, setWindowSize] = React.useState();
React.useEffect(() => {
window.addEventListener("resize", () => {
setWindowSize({ width: window.innerWidth, height: window.innerHeight });
});
}, []);
}
How to add SSR support
However, the code as we have it here will not work. And this is because a key rule of hooks is that they cannot be called conditionally. As a result, we cannot have a conditional above our useState or useEffect hook, before they are called.
So to fix this, we’ll set the initial value of useState conditionally. We’ll create a variable called isSSR, which will perform the same check to see if the window is not equal to the string undefined.
And we’ll use a ternary to set the width and height by first checking to see if we’re on the server. If we are we’ll use the default value. If not, we’ll use window.innerWidth and window.innerHeight.
// utils/useWindowSize.js
import React from "react";
export default function useWindowSize() {
// if (typeof window !== "undefined") {
// return { width: 1200, height: 800 };
// }
const isSSR = typeof window !== "undefined";
const [windowSize, setWindowSize] = React.useState({
width: isSSR ? 1200 : window.innerWidth,
height: isSSR ? 800 : window.innerHeight,
});
React.useEffect(() => {
window.addEventListener("resize", () => {
setWindowSize({ width: window.innerWidth, height: window.innerHeight });
});
}, []);
}
Then finally, we need to think about when our components unmount. What do we need to do? We need to remove our resize listener.
Removing resize event listener
You can do that by returning a function from useEffectand we will remove the listener with window.removeEventListener.
// utils/useWindowSize.js
import React from "react";
export default function useWindowSize() {
// if (typeof window !== "undefined") {
// return { width: 1200, height: 800 };
// }
const isSSR = typeof window !== "undefined";
const [windowSize, setWindowSize] = React.useState({
width: isSSR ? 1200 : window.innerWidth,
height: isSSR ? 800 : window.innerHeight,
});
React.useEffect(() => {
window.addEventListener("resize", () => {
setWindowSize({ width: window.innerWidth, height: window.innerHeight });
});
return () => {
window.removeEventListener("resize", () => {
setWindowSize({ width: window.innerWidth, height: window.innerHeight });
});
};
}, []);
}
But we need a reference to the same function, not two different ones as we have here. To do that, we’ll create a shared callback function to both of the listeners called changeWindowSize.
And finally, at the end of the hook, we will return our windowSize state. And that’s it.
// utils/useWindowSize.js
import React from "react";
export default function useWindowSize() {
const isSSR = typeof window !== "undefined";
const [windowSize, setWindowSize] = React.useState({
width: isSSR ? 1200 : window.innerWidth,
height: isSSR ? 800 : window.innerHeight,
});
function changeWindowSize() {
setWindowSize({ width: window.innerWidth, height: window.innerHeight });
}
React.useEffect(() => {
window.addEventListener("resize", changeWindowSize);
return () => {
window.removeEventListener("resize", changeWindowSize);
};
}, []);
return windowSize;
}
How to use the hook
To use the hook, we just need to import it where we need, call it, and use the width wherever we want to hide or show certain elements.
In my case, this is at the 500px mark. There, I want to hide all of the other links and only show the Join Now button, like you see in the example above:
// components/StickyHeader.js
import React from "react";
import useWindowSize from "../utils/useWindowSize";
function StickyHeader() {
const { width } = useWindowSize();
return (
<div>
{/* visible only when window greater than 500px */}
{width > 500 && (
<>
<div onClick={onTestimonialsClick} role="button">
<span>Testimonials</span>
</div>
<div onClick={onPriceClick} role="button">
<span>Price</span>
</div>
<div>
<span onClick={onQuestionClick} role="button">
Question?
</span>
</div>
</>
)}
{/* visible at any window size */}
<div>
<span className="primary-button" onClick={onPriceClick} role="button">
Join Now
</span>
</div>
</div>
);
}
This hook will work on any server rendered React app, such as Gatsby and Next.js.