CSS is the perfect tool when it comes to creating responsive websites and apps, that’s not going to change any time soon. However, sometimes in a React application, you need to conditionally render different components depending on the screen size.
Wouldn’t it be great if instead of having to reach for CSS and media queries we could create these responsive layouts right in our React code? Let’s take a quick look at a naive implementation of something like this, to see exactly what I mean:
const MyComponent = () => { // The current width of the viewport const width = window.innerWidth; // The width below which the mobile view should be rendered const breakpoint = 620; /* If the viewport is more narrow than the breakpoint render the mobile component, else render the desktop component */ return width < breakpoint ? <MobileComponent /> : <DesktopComponent />; }
This simple solution will certainly work. Depending on the window width of the user’s device we render either the desktop or mobile view. But there is a big problem when the window is resized the width value is not updated, and the wrong component could be rendered!
We are going to use React Hooks to create an elegant and, more importantly, reusable solution to this problem of creating responsive layouts in React. If you haven’t used React Hooks extensively yet, this should be a great introduction and demonstration of the flexibility and power that Hooks can provide.
The problem with the example shown above is that when the window is resized the value of width
is not updated. In order to solve this issue, we can keep track of width
in React state and use a useEffect
Hook to listen for changes in the width of the window:
const MyComponent = () => { // Declare a new state variable with the "useState" Hook const [width, setWidth] = React.useState(window.innerWidth); const breakpoint = 620; React.useEffect(() => { /* Inside of a "useEffect" hook add an event listener that updates the "width" state variable when the window size changes */ window.addEventListener("resize", () => setWidth(window.innerWidth)); /* passing an empty array as the dependencies of the effect will cause this effect to only run when the component mounts, and not each time it updates. We only want the listener to be added once */ }, []); return width < breakpoint ? <MobileComponent /> : <DesktopComponent />; }
Now whenever the window is resized the width
state variable is updated to equal the new viewport width, and our component will re-render to show the correct component responsively. So far so good!
There is still a small problem with our code, though. We are adding an event listener, but never cleaning up after ourselves by removing it when it is no longer needed. Currently when this component is unmounted the “resize” event listener will linger in memory, continuing to be called when the window is resized and will potentially cause issues. In old school React you would remove the event listener in a componentWillUnmount
lifecycle event, but with the useEffect
Hook all we need to do is return a cleanup function from our useEffect
.
const MyComponent = () => { const [width, setWidth] = React.useState(window.innerWidth); const breakpoint = 620; React.useEffect(() => { const handleWindowResize = () => setWidth(window.innerWidth) window.addEventListener("resize", handleWindowResize); // Return a function from the effect that removes the event listener return () => window.removeEventListener("resize", handleWindowResize); }, []); return width < breakpoint ? <MobileComponent /> : <DesktopComponent />; }
This is looking good now, our component listens to the window resize event and will render the appropriate content depending on the viewport width. It also cleans up by removing the no longer needed event listener when it un-mounts.
This is a good implementation for a single component, but we most likely want to use this functionality elsewhere in our app as well, and we certainly don’t want to have to rewrite this logic over and over again every time!
Custom React Hooks are a great tool that we can use to extract component logic into easily reusable functions. Let’s do this now and use the window resizing logic we have written above to create a reusable useViewport
Hook:
const useViewport = () => { const [width, setWidth] = React.useState(window.innerWidth); React.useEffect(() => { const handleWindowResize = () => setWidth(window.innerWidth); window.addEventListener("resize", handleWindowResize); return () => window.removeEventListener("resize", handleWindowResize); }, []); // Return the width so we can use it in our components return { width }; }
You’ve probably noticed that the code above is almost identical to the code we wrote before, we have simply extracted the logic into its own function which we can now reuse. Hooks are simply functions composed of other Hooks, such as useEffect
, useState
, or any other custom Hooks you have written yourself.
We can now use our newly written Hook in our component, and the code is now looking much more clean and elegant.
const MyComponent = () => { const { width } = useViewport(); const breakpoint = 620; return width < breakpoint ? <MobileComponent /> : <DesktopComponent />; }
And not only can we use the useViewport
Hook here, we can use it in any component that needs to be responsive!
Another great thing about Hooks is that they can be easily extended. Media queries don’t only work with the viewport width, they can also query the viewport height. Let’s replicate that behaviour by adding the ability to check the viewport height to our Hook.
const useViewport = () => { const [width, setWidth] = React.useState(window.innerWidth); // Add a second state variable "height" and default it to the current window height const [height, setHeight] = React.useState(window.innerHeight); React.useEffect(() => { const handleWindowResize = () => { setWidth(window.innerWidth); // Set the height in state as well as the width setHeight(window.innerHeight); } window.addEventListener("resize", handleWindowResize); return () => window.removeEventListener("resize", handleWindowResize); }, []); // Return both the height and width return { width, height }; }
That was pretty easy! This Hook is working well now, but there is still room for improvement. Currently, every component that uses this Hook will create a brand new event listener for the window resize event. This is wasteful, and could cause performance issues if the Hook were to be used in a lot of different components at once. It would be much better if we could get the Hook to rely on a single resize event listener that the entire app could share.
We want to improve the performance of our useViewport
Hook by sharing a single-window resize event listener amongst all the components that use the Hook. React Context is a great tool in our belt that we can utilize when state needs to be shared with many different components, so we are going to create a new viewportContext
where we can store the state for the current viewport size and the logic for calculating it.
const viewportContext = React.createContext({}); const ViewportProvider = ({ children }) => { // This is the exact same logic that we previously had in our hook const [width, setWidth] = React.useState(window.innerWidth); const [height, setHeight] = React.useState(window.innerHeight); const handleWindowResize = () => { setWidth(window.innerWidth); setHeight(window.innerHeight); } React.useEffect(() => { window.addEventListener("resize", handleWindowResize); return () => window.removeEventListener("resize", handleWindowResize); }, []); /* Now we are dealing with a context instead of a Hook, so instead of returning the width and height we store the values in the value of the Provider */ return ( <viewportContext.Provider value={{ width, height }}> {children} </viewportContext.Provider> ); }; /* Rewrite the "useViewport" hook to pull the width and height values out of the context instead of calculating them itself */ const useViewport = () => { /* We can use the "useContext" Hook to acccess a context from within another Hook, remember, Hooks are composable! */ const { width, height } = React.useContext(viewportContext); return { width, height }; }
Make sure you also wrap the root of your application in the new ViewportProvider
, so that the newly rewritten useViewport
Hook will have access to the Context when used further down in the component tree.
const App = () => { return ( <ViewportProvider> <AppComponent /> </ViewportProvider> ); }
And that should do it! You can still use the useViewport
Hook in exactly the same way as before, but now all the data and logic is kept in a single tidy location, and only one resize event listener is added for the entire application.
const MyComponent = () => { const { width } = useViewport(); const breakpoint = 620; return width < breakpoint ? <MobileComponent /> : <DesktopComponent />; }
Easy peasy. Performant, elegant, and reusable responsive layouts with React Hooks 🎉
Our Hook is working but that doesn’t mean we should stop working on it! There are still some improvements that could be made, but they fall outside the scope of this post. If you want to get extra credit (although no one is counting) here are some ideas for things you could do to improve this Hook even further:
window
exists before trying to access itWindow.matchMedia
browser API could provide a better solution to this problem than checking the width of the window. The Hook could be extended to support this tooI have created a Code Sandbox which contains the completed code for this tutorial.
I hope that this article has helped you to learn more about React Hooks and how their flexibility can be leveraged to achieve all kinds of exciting functionality in your apps in a clean and reusable way. Today we have used them to build responsive layouts without needing CSS media queries, but they can really be used for any number of use cases. So get creative!
Happy coding. ✌️
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
21 Replies to "Developing responsive layouts with React Hooks"
This is a great read! Just what I needed.
Why do this in react with so many lines of JavaScript when you could use a few lines of css media queries?
Your post misses how to export and import from the context since it’s all one component in your example
There is no clue to emulate media queries via using React Hooks. Better thing that you can do using this method is kind of container queries emulation — when you watching for width used by component, not window width.
Hey Omar, I completely agree with you, CSS should almost always be the first tool you reach for to do responsive design.
Saying that, there are reasons to use both, in the right circumstances.
Firstly you need to ask if what you are changing on mobile vs desktop is a style change, or if it is application logic. If it is application logic and you need to do any kind of calculation or complicated conditional rendering using the viewport size then putting that logic in your JavaScript code rather than writing complex media queries in CSS could be beneficial.
Secondly, and more importantly, hiding content with CSS doesn’t actually remove it from the virtual DOM. This means that React is still going to be re-rendering components that aren’t actually being used, so if you are showing drastically different components for different viewport sizes it could actually improve your performance to not render those components at all!
Hope that helps.
Hey Alexander, Yeah that’s a good idea. You could achieve what you are describing by using the React.useRef Hook. You could then pass the ref into a custom hook of your own design that returns the dimensions of your component based on that ref.
It’s one reusable component, once that code is in place, it’s a simple 2 line solution that allows you to render completely different content. Something that you can’t do nice and cleanly with CSS media queries.
Thank you much this great write up of how to use React Effects, Hooks, and Context.
Really great post! It’s a much cleaner solution than writing tons of media queries and `display: none` declarations when dealing with complex content layout shifts between mobile & desktop views 🙂
Everything works perfectly in my development environment, but I’m running into an issue implementing this in production with Gatsby because of how Gatsby is handling SSR.
I am obviously getting a `window` is undefined error, which is expected. So I created a simple utility function:
`export const isSSR = typeof window === ‘undefined’;`
And used it like:
“`
const [width, setWidth] = React.useState(isSSR ? 0 : window.innerWidth);
const [height, setHeight] = React.useState(isSSR ? 0 : window.innerHeight);
“`
This gives us a fallback value of 0 on server side and allows me to successfully execute a `gatsby build` command. The issue, however, is that once the content is served up in production, I am not getting the correct component returned.
It seems that SSR is still somehow returning true on the client side and preventing values from being set/correct components from being rendered until the window is resized enough to manually trigger a component swap, then everything works as intended when resizing.
When I refresh the page, I end up with broken components again.
Any guidance here would be much appreciated. I imagine a lot of people are dealing with when implementing with Gatsby or NextJS.
Thank you so much!!
I created a pastebin of my full implementation for reference:
https://pastebin.com/phwNUqjN
As a follow-up here… after some digging, it seems that this approach is not well suited for any SSR solution, unless there are workarounds that I am unaware of.
The issue is how client-side re-hydration happens. Since HTML is server-generated for Gatsby and server-rendered for NextJS, there obviously can be no access to `window`. In these cases, we are having to generate a default view of either mobile or desktop (depending on how your responsive rendering component is configured) and then have the component rehydrate client-side once `useEffect` fires. When this happens there is an obvious flash between the default server-rendered view, and the properly rendered view based on `window.innerWidth` in the client.
We are also losing the SEO and performance benefits of SSR in this case, because the entire component is essential just rendering on the client side :/
CSS media queries are fine when you have the same component for every screen size but just with minor changes to it (width, padding, etc) but what do you do when you have a component for a screen size and another component completely different for another screen size? For example, you have a full navbar with one link besides the other for desktop and an hamburger menu for mobile. In this case you need a conditional render, css media queries will not do the job.
Amazing post, thank you so much.
Can you explain how the cleanup happens in the useEffect? I see it returns the function, but how is it called?
Chris,
I know it’s been a few months, but you might look into the package below. It renders all breakpoints on the server, where then, only the correct one is displayed on the client.
https://github.com/artsy/fresnel
Hey Kelly,
That is just part of how the useEffect hook works in React. You can find the appropriate part of documentation about this here:
https://reactjs.org/docs/hooks-effect.html#example-using-hooks-1
Thanks for reading 😊
Hi Chris, did you find some workaround for this issue? im having the same problem i cant find more info to this day.
Thank you!
Ben, thanks for the great complete article! And Chris, thank you for the great note about possible issues with Gatsby, SSR and SEO.
I’m happy to say that I’ve had success getting it working in my Gatsby project, including the built/rehydrated
version! Achieved it using the browser-monads package to detect if window variable exists (https://www.npmjs.com/package/browser-monads)
I ended up adding the ViewportProvider inside my Layout component. This comes with a caveat that useViewport() hook can’t be called directly from the Page component (since Layout is a child of Page). However ViewportContext.Consumer does work directly in the Page if it’s nested under the Layout component; and the useViewport() hook does work in any of child of the Page component.
Our team’s use case is primarily swapping out fairly similar content, but for SEO I think will be to be careful to always render the SEO-preferable content by default (which I think is generally a good habit anyway.)
I think one way to improve performance would be if instead of having the hook return the width, you could have the hook return whether or not the breakpoint was hit. With the current setup the state is being updated every time the width changes, but you don’t actually need that much granularity, you just need to know if the breakpoint was hit. The width could be stored in a ref instead that doesn’t trigger an update and the hook could be modified to be something like `useBreakpoint(width)` with a boolean breakpoint state instead.
Hey Tim. I mention in the article that the Window.matchMedia browser API could provide a better solution to this problem than checking the width of the window. If you don’t need the actual window width and just need the breakpoint then I would suggest making a hook that uses that approach.
https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia
https://codesandbox.io/s/quizzical-sun-tqgvgt
In a real application, the logic is divided among several (many) components, so in order to be able to use the technique you described, you need to separate the code into a separate component, which I did. I also rewrote for the latest version of react. please see the working code , and if you can improve. because I’m just a beginner.
By the way, the handling of {children} has changed in React 18
Translated with http://www.DeepL.com/Translator (free version)