DEV Community

Cover image for Developing responsive layouts with React Hooks
Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

Developing responsive layouts with React Hooks

Written by Ben Honeywill✏️

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 />;
}
Enter fullscreen mode Exit fullscreen mode

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.

LogRocket Free Trial Banner

Initial implementation using Hooks

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 />;
}
Enter fullscreen mode Exit fullscreen mode

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 />;
}
Enter fullscreen mode Exit fullscreen mode

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!

Making the logic reusable with a custom Hook

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 };
}
Enter fullscreen mode Exit fullscreen mode

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 />;
}
Enter fullscreen mode Exit fullscreen mode

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 };
}
Enter fullscreen mode Exit fullscreen mode

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.

Optimizing performance with a Context

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 };
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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 />;
}
Enter fullscreen mode Exit fullscreen mode

Easy peasy. Performant, elegant, and reusable responsive layouts with React Hooks. 🎉

Other considerations

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:

  • Improving performance by throttling the window resize event listener so that there are fewer re-renders while resizing the browser window
  • Edit the Hook so that it supports server-side rendering. This could be achieved by checking window exists before trying to access it
  • The Window.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 too

Conclusion

I 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. ✌


Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

Alt Text

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — start monitoring for free.


The post Developing responsive layouts with React Hooks appeared first on LogRocket Blog.

Top comments (2)

Collapse
 
jannikwempe profile image
Jannik Wempe

Thanks for the awesome article. I learned something and love hooks more and more ;) Adding throttling would make it an awesome solution as a drop in to various projects...

I couldn't make it work with lodashs throttling (there is no actual throttling in my solution). Here is what i got:

 React.useEffect(() => {
    const handleWindowResize = () => {
      console.log(count); // just a counter I added as a state
      setWidth(window.innerWidth);
      setHeight(window.innerHeight);
      setCount(prevCount => prevCount + 1);
    };
    const throttledHandleWindowResize = _.throttle(handleWindowResize, 1000);

    window.addEventListener("resize", throttledHandleWindowResize);

    return () =>
      window.removeEventListener("resize", throttledHandleWindowResize);
  }, [count]);

I guess it is because every time count changes a new event listener is attached and therefore throttling never happens. Am I right? What would be the solution? I guess it is something using useRef()? But I don't get it...

Collapse
 
benhoneywill profile image
Ben Honeywill • Edited

Hey Jannik, author here. Sorry for the very late reply I wasn't aware that my article was posted here 🙂 I'm glad you found it useful!

You are right, every time you are updating count your useEffect is going to rerun and a different event listener is going to be added every time. Moving your event listener outside of the effect should solve this for you!

You might also want to make use of useCallback, so that you can safely pass the function into the effect as a dependency - it would look something like this:

const throttledHandleWindowResize = React.useCallback(
    _.throttle(() => {
      setWidth(window.innerWidth);
      setHeight(window.innerHeight);
    }, 1000),
    [setWidth, setHeight]
);

React.useEffect(() => {
    window.addEventListener("resize", throttledHandleWindowResize);
    return () => window.removeEventListener("resize", throttledHandleWindowResize);
}, [throttledHandleWindowResize]);

Here are the useCallback docs, if you want to have a read about this 🙂Thanks for reading!