React – Combining Server-Side Rendering and Responsive Design

Published on February 17, 20206 min read

Let’s explain the challenge that comes up when mixing SSR and responsive design, and introduce a couple of possible strategies to approach the problem – inspired by CSS Media Queries, userAgent and such.

Progressive web applications (PWA) are being developed more and more for the last few years. It’s not a surprise, especially when applications like these are expected to have cross-platform support and actually to function everywhere. In order to make it happen, these applications tend to implement principles of discoverability, progressiveness, responsiveness and so on.

Even though that PWAs aren’t based on a specific technology, but merely an approach with principles to build web applications – they involve technical ecosystems, Web APIs and practices. Integrating React’s ecosystem, for instance, with the available Web APIs – allows us to make a fully-responsive application that’s also discoverable and well-performed. Sometimes, however, it’s not so straightforward to mix them together, both for technical and conceptual aspects.

This article will specifically focus on the challenge that might arise when implementing an application that’s rendered by the server-side, but on the other hand, needs to behave responsively on different platforms.

Note: We’re going to use React mainly to describe the problem comfortably and to demonstrate simply the ideas hiding beneath the possible strategies.

The Challenge

In order to deeply understand the challenge, we should be familiar at first with the following terms:

  • Server-Side Rendering – a technique to render client-side applications on the server thereby returning a complete HTML markup, which was rendered in advance, to the client (in a nutshell, the major benefits are better SEO and performance)
  • Responsive Design – an approach to design and implement web applications which flexibility respond (visually or behaviorally) to the device that renders them so that they keep looking well and functioning depending on the device constraints

Having said that, mixing SSR with applied responsive concepts definitely pretends to be a challenge.

Let’s explain why. 🤔

Lack of A Browser

The server doesn’t recognize the window neither document. This means that the device, in other words, cannot detect obligatory properties (such as the viewport dimensions of the client) – thereby it needs to infer them someway, which means, a pretty limited and non-accurate way to respond.

For instance, imagine we’ve an application that uses matchMedia (which, as you probably know, is a Web API that arrives on top of thewindow) to render components conditionally based on the viewport dimensions. How would you expect the server to render the markup without thewindow, and even if it’s hypothetically polyfilled somehow, what about the dimensions? How would it respond once the initial render contains a responsive component that conditionally affected by a breakpoint?

Put it simply – this might cause the server to render our application incorrectly, which eventually, leads to partial hydration that patches the mismatches (namely potential bugs?).

Performance

As we already mentioned, one of the major benefits of SSR is allowing better performance (by preparing the markup for the client ahead of time).

So, whatever strategy would be adopted – we still want that to be performed efficiently, because otherwise – we detract a substantial benefit of SSR.

The challenge becomes even more complicated. 🤦🏻‍♂️

Possible Strategies

Well, we apparently understand now that rendering a responsive application on the server-side isn’t really trivial and raises interesting questions.

Let’s introduce a couple of possible strategies while examining if they meet our challenge.

Restricting Initial Render

The easiest way to deal is probably to prevent from the initial render to render components conditionally based on device properties. This doesn’t mean avoiding responsiveness completely – but just not making the entry component (and its child components) a device-based conditional.

Basically, this strategy says that the initial render wouldn’t produce any different DOM trees for various device properties, whereas different styles (such as fontSize, margin, width, etc.) are unrestrictedly permitted:

// Styles
.layout {
  width: 80vh;
}

@media screen and (max-width: 768px) {
  .layout {
    width: 100vh;
  }
}

// JSX
const App = () => (<div className="layout">My Application</div>);

We merely style the basic "layout" above according to the viewport width, nothing else. Indeed, the DOM tree remains the same so mismatches aren’t expected.

The thing is, that sometimes we do ought that our entry component would be conditioned by a device property – thus, in this regard, we need to think about another strategy.

Sniffing User-Agent

The User-Agent is a request header provides essential information of the requesting client, especially for certain recipient (usually a server):

Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36

As we notice, it’s just a string describing the operating system, engine, browser – including their versions.

Additionally, we better know that most of the mobile devices concatenate "Mobile" keyword to the userAgent, which controversially could serve our server in order to detect wisely the device type of the client:

// Server
const uaParser = require('ua-parser-js');

const userAgent = uaParser(req.headers['user-agent']);
const { type } = userAgent.getDevice();

const html = ReactDOMServer.renderToString(
  <DeviceContext.Provider type={{ type }}>
    <App />
  </DeviceContext.Provider>
);

// Client
const App = () => (
  <DeviceContext.Consumer>
    {({ type }) => (type === 'mobile' ? <MobileLayout /> : <DesktopLayout />)}
  </DeviceContext.Consumer>
);

The example above shows a server that retrieves the userAgent out of the request header. Also, we use a library called UAParser.js to parse and extract the device type easily – which is provided eventually to the client application using Context API. Then, we merely consume the extracted device type and conditionally render the appropriate "layout" component based on that.

Sadly, there are a few drawbacks to this strategy. 😞

First of all, the information we can extract out of the userAgent is pretty limited. Namely, we cannot understand the device properties (dimensions, orientation, etc.) directly. Indeed, it might be possible to infer the viewport dimensions someway, however then again – it leaves the orientation (and others) unsolved.

Secondly, it doesn’t cover the scenario we render different components per each breakpoint. Obviously, we can say that our server infers the dimensions independently using the provided type and thereafter passes them to the application to be used as defaults – but practically, that’s not doable in case of a variety of breakpoints.

Thirdly, we assumed that the userAgent includes “Mobile” keyword on mobile devices whereas it’s not completely true. Some of the browsers concatenate unique variations instead; such as "Mobi", "IEMobile" and even "Tablet". Although libraries like UAParser.js typically consider them, that’s a concern we should recognize. And worse, these keywords might be missing when rendering using a desktop browser that’s resized to mobile dimensions – which would probably lead to DOM tree mismatches.

Using Pure CSS

CSS Media Queries are exposed through particular at-rules that allow applying styles conditionally.

Theoretically, we can ditch the conditional DOM rendering and use CSS only:

// Styles
@media screen and (min-width: 769px) {
  .mobile-layout {
    display: none;
  }
}

@media screen and (max-width: 768px) {
  .desktop-layout {
    display: none;
  }
}

// JSX
const App = () => (
  <>
    <div className="mobile-layout">
      <MobileLayout />
    </div>
    <div className="desktop-layout">
      <DesktopLayout />
    </div>
  </>
);

In this example, the server renders all components despite the breakpoints – so that both "layout" components are part of the rendered markup.

When it returns to the client, the markup is hydrated and the appropriate components (based on the viewport dimensions) are displayed. The hidden elements, however, are omitted from the render tree (as opposed to the DOM tree) and that’s why the browser doesn’t paint them after all.

Admittedly, the initial render would succeed this time. That said, imagine each layout component is built from lots of other components – so now, all of those components would be rendered in the DOM tree, and many unnecessarily. Besides the fact it leads to a larger DOM payload, there are components that are definitely mounted but not displayed (and this becomes even worse when those perform side-effects).

It’s noteworthy the @artsy/fresnel library, that affectively takes this strategy but makes it much better – on top of React. Although all elements would still be rendered in the DOM, only the components which match the breakpoint would be mounted. In case the viewport dimensions change, the appropriate component would be mounted instead. This means the potential needless side-effects are eliminated, while rendering the exact matching component. 👏🏻

Summary

We explained today the challenge that comes up when combining server-side rendering and responsive design.

Let’s recap:

  • The server-side cannot detect the device properties without the aid of the client, and even if – the information is limited and non-accurate
  • The server-side cannot render the markup in case the initial render contains device-based conditional components
  • We can restrict the initial render to not include device-based conditional components
  • We can sniff the userAgent to extract the device type, however:
    • Not all the device properties are inferable
    • The amount of the inferable breakpoints is limited
    • There might be unique keywords represent a browser of mobile device
  • We can use CSS to apply device-based conditional styles, however:
    • Hidden elements would be still rendered in the DOM and mounted
    • A concern of larger DOM payload
    • A concern of needless side-effects
  • We can use @artsy/fresnel to render only the matching component for a device-based conditional style

In conclusion, perhaps you expected to read the perfect strategy – but as you probably understand; it’s absolutely a trade-off which mainly depends on the use-case, considering the good points and bad points. 😉

Follow Me

Join My Newsletter

Get updates and insights directly to your inbox.

Site Navigation


© 2024, Nitay Neeman. All rights reserved.

Licensed under CC BY 4.0. Sharing and adapting this work is permitted only with proper attribution.