Using CSS Media Queries in React with Fresnel

📅 2 years ago 🕒 11 min read 🙎‍♂️ by Madza

Using CSS media queries in React with Fresnel

According to StatCounter data the device market of today is dominated by mobile, desktop, and tablets. In order to provide the best UX for the end-users, responsive design is a must-have in modern web development.

In this article, we will take a closer look at Fresnel package, which is one of the most efficient ways to implement responsive design for server-side rendering (SSR). It is an open-source NPM project created by Artsy, that is easy to use and trusted by developers:

Fresnel npm trends image

We will explore what it does differently from the traditional approaches and why you should consider to use it. We will also create a responsive color cards app in React to demonstrate its features in practice.

What are media queries?

Media queries allow developers to define different styling rules for different viewport sizes.

Normally we would use the traditional approach where we first create the HTML element and then use CSS to describe how it would behave on different screen widths via media queries.

A simple example would be:

<div class="wrapper"></div>

And then via CSS:

.wrapper {
  width: 300px;
  height: 300px;
  background: gold;
}

@media screen and (min-width: 480px) {
  .wrapper {
    background-color: lightgreen;
  }
}

If we run an example on JSBin, we see that the square element change its background color when the 480px width of the viewport is been met:

JSbin GIF

What does Fresnel do differently?

Fresnel transfers the traditional media query approach to the React ecosystem.

Its breakpoint logic will be beneficial when the app needs to be scaled and the complexity of components grows.

A basic implementation would look like this:

import React from "react"
import ReactDOM from "react-dom"
import { createMedia } from "@artsy/fresnel"

const { MediaContextProvider, Media } = createMedia({
  breakpoints: {
    sm: 0,
    md: 768,
    lg: 1024,
    xl: 1192,
  },
})

const App = () => (
  <MediaContextProvider>
    <Media at="sm">
      <MobileComponent />
    </Media>
    <Media at="md">
      <TabletComponent />
    </Media>
    <Media greaterThanOrEqual="lg">
      <DesktopComponent />
    </Media>
  </MediaContextProvider>
)

ReactDOM.render(<App />, document.getElementById("react"))

Thanks to its API, we can use MediaContextProvider and Media components to build a solid foundation for a responsive web application.

The behavior on certain screen widths is controlled by providing the defined breakpoints as props. Those include at, lessThan, greaterThan, greaterThanOrEqual, and between and are self-explanatory by their names.

Isn't that just conditional rendering?

If you have implemented responsive layouts in React before, the code structure of example above might look familiar. Chances are you have used conditional rendering, like this:

const App = () => {
  const { width } = useViewport();
  const breakpoint = 768;

  return width < breakpoint ? <MobileComponent /> : <DesktopComponent />;
}

The above example would work fine unless you would need to implement a server-side rendering solution. That's where the Fresnel comes in and distinguishes itself from other solutions.

To explain the concept, server-side rendering (SSR) is a technique to render client-side single-page applications (SPA) on the server. This way the client receives a rendered HTML file.

With Fresnel we can render all the breakpoints on the server, so we can properly render HTML/CSS before the React has booted. This improves the UX for the end-users.

What will we build?

We will create a color card application that switches its layout for the mobile and desktop views.

The cards on the mobile view will be positioned in the single-column layout, while the desktop view will use a more complex grid-style layout alternating between horizontal and vertical cards.

The wireframe of the project, displaying the sequence of cards:

Wireframe of the project

Initialize the app

We will start by creating a separate folder for our project and change the working direction into it. To do that, run the following command in the terminal: mkdir fresnel-demo && cd fresnel-demo.

To initialize a new project run npm init -y.

Notice that the -y tag will approve all the default values for the package configuration, so you do not have to go through a multistep wizard in the terminal.

Next, we will install the fundamental prerequisites for the front end of the app. Run npm i react react-dom.

We will base the backend on the Express framework. To install it run the command npm i express.

While we are at the root, let's install the Fresnel library itself as well, so we are good to go at later steps. To do that run the command npm i @artsy/fresnel.

Modelling data

Since we will be building the color app, the main data we will need will be the color names.

It's always a great practice to separate the data from the app logic. For this reason, we will start by creating src folder and inside it another folder called data.

In the data folder create a new file called colors.jsx and include the following code:

export const colors = [
  "gold",
  "tomato",
  "limegreen",
  "slateblue",
  "deeppink",
  "dodgerblue",
];

We created the colors variable with all the color names stored as an array of strings.

Creating the breakpoints

Now we must define which breakpoints our app will use.

Inside the src forlder create another folder media. Inside it create a new file breakpoints.jsx and include the following code:

import { createMedia } from "@artsy/fresnel";

const ExampleAppMedia = createMedia({
  breakpoints: {
    sm: 0,
    md: 768,
    lg: 1024,
    xl: 1192,
  },
});

export const mediaStyle = ExampleAppMedia.createMediaStyle();
export const { Media, MediaContextProvider } = ExampleAppMedia;

We used createMedia function to define specific breakpoints and exported mediaStyle that we will later inject into the server-side as well as Media and MediaContexProvider to wrap around the components that need to be responsive.

Creating components

Inside the same src folder create another folder components.

Inside the components forder create 4 separate files: DesktopComponent.jsx, DesktopComponent.css, MobileComponent.jsx and MobileComponent.css.

Open the DesktopComponent.jsx file and include the following code:

import React from "react";
import "./DesktopComponent.css";
import { colors } from "../data/colors";

export const DesktopComponent = () => {
  return (
    <div className="dWrapper">
      {colors.map((el, i) => (
        <div
          style={{ backgroundColor: el, gridArea: `c${i + 1}` }}
          key={i}
        ></div>
      ))}
    </div>
  );
};

We imported the external style sheet and the color names. In the component, we looped through all the colors and assigned the background color as well as position in the grid.

Then open DesktopComponent.css and add the following style rules:

.dWrapper {
  max-width: 1200px;
  height: 500px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-template-rows: repeat(3, 1fr);
  gap: 20px;
  grid-template-areas:
    "c1 c1 c2 c3"
    "c4 c5 c2 c3"
    "c4 c5 c6 c6";
}

We set the max-width and height for the wrapper and centered it in the viewport. Then we used grid templates to define the columns and rows and created the layout schema.

Next, open the MobileComponent.jsx file and include the following code:

import React from "react";
import "./MobileComponent.css";
import { colors } from "../data/colors";

export const MobileComponent = () => {
  return (
    <div className="mWrapper">
      {colors.map((el, i) => (
        <div style={{ backgroundColor: el }} key={i}></div>
      ))}
    </div>
  );
};

We imported external styles and color names. Then we looped through the colors and assigned background colors for each element.

Finally, open MobileComponent.css and add the following style rules:

.mWrapper {
  width: 100%;
  display: grid;
  grid-template-rows: repeat(6, 100px);
  gap: 20px;
}

We set the width to fill the whole viewport, used a grid system for the rows as well as added some gaps between them.

Implementing frontend

Now let's create an actual App component that will render the components we created earlier. Inside the src folder create a new file App.jsx and include the following code:

import React from "react";
import { Media, MediaContextProvider } from "./media/breakpoints";
import { MobileComponent } from "./components/MobileComponent";
import { DesktopComponent } from "./components/DesktopComponent";

export const App = () => {
  return (
    <MediaContextProvider>
      <Media at="sm">
        <MobileComponent />
      </Media>
      <Media greaterThan="sm">
        <DesktopComponent />
      </Media>
    </MediaContextProvider>
  );
};

We imported Media and MediaContextProvider from the breakpoints.jsx file and used them to control which components should be displayed on which viewports.

In order to be able to render the App to the screen, we will need a base file that will access the root element of the DOM tree and render it into it. Create a new file index.jsx in the src folder and include the following code in it:

import React from "react";
import ReactDOM from "react-dom";
import { App } from "./App";

ReactDOM.hydrate(<App />, document.getElementById("root"));

Notice that we used hydrate instead of render. This is the recommended way for server-side rendering since it turns server-rendered HTML into a dynamic web page by attaching event handlers.

Setting up the backend

Now let's switch our focus from the frontend to the backend. Navigate back to the root of the project and create a new folder called server.

Inside the newly created server folder create a single file index.jsx. A single file will be enough to provide the functionality for the server-side rendering.

Include the following code:

import React from "react";
import ReactDOMServer from "react-dom/server";
import { App } from "../src/App";
import { mediaStyle } from "../src/media/breakpoints";

import express from "express";
const app = express();
const PORT = 3000;

app.get("/", (req, res) => {
  const app = ReactDOMServer.renderToString(<App />);

  const html = `
        <html lang="en">
        <head>
        <title>Fresnel SSR example</title>
        <style type="text/css">${mediaStyle}</style>
        <link rel="stylesheet" href="app.css">
        <script src="app.js" async defer></script>
        </head>
        <body>
            <div id="root">${app}</div>
        </body>
        </html>
    `;
  res.send(html);
});

app.use(express.static("./built"));

app.listen(PORT, () => {
  console.log(`App started on port ${PORT}`);
});

First we created an instance of Express, which we assigned to port 3000. Then for all the incoming GET requests on the / route, we used renderToString() function to generate HTML on the server and send the markup as a response.

Notice that we also injected mediaStyle into the head section of the HTML. This is how the Fresnel will be able to render the breakpoints on the server.

Configure builds

Before we can run our app, we will need to bundle our files so we can access them during SSR. We will use esbuild, which is a very fast bundler.

First, install it by running the command: npm i --dev esbuild

In the project root, open the file package.json and set the scripts to the following:

"scripts": {
    "client:build": "esbuild src/index.jsx --bundle --outfile=built/app.js",
    "server:build": "esbuild server/index.jsx --bundle --outfile=built/server.js --platform=node",
    "start": "node built/server.js"
  }

We will first have to run the build script for the frontend. Use the command npm run client:build. That will generate a new folder built with app.js and app.css files in it.

Next, we will have to do the same for the server files. Run npm run server:build.

Testing the app

If you have followed all the previous steps, all you have to do to start the app is to run the command npm start. You will receive the message in the terminal from the server.jsx file informing you that the developer server is ready on port 3000.

Now open your browser and navigate to http://localhost:3000. You should be presented with the responsive app. Open the dev tools by pressing F12 on your keyboard and try to resize the browser view:

GIF of responsive layout

Be aware that if you want to make any changes on the frontend or server, you will have to rebuild the app and restart the server. Alternatively, you can use --watch flags at the end of the build commands, more instructions on that here.

Conclusion

In this article, we explored the Fresnel package and built a fully responsive web application.

We also learned a lot of other things, like how to do set up the React project from scratch without using external tools like CRA, how to set up SSR rendering in React, and how to work with builders like esbuild.

Next time you need to provide the best rendering experience on React apps, you will have a secret method in your toolbox. You will know it's possible by rendering the media breakpoints on the server via Fresnel package.