Bayan Bennett

JavaScript-less Static SSR using Styled Components

Building a site with React, but that doesn't have JavaScript in the final build is a paradox.

Requirements

I had a special case come up where I needed to build a site that:

  • Only had static HTML
  • Didn't have any JavaScript whatsoever

Weapon of Choice

React still was an attractive option due to its composability and the widespread availability of libraries. Other JS libraries (Vue | Angular | Svelte) might work as well, but I am not familiar with their SSR capabilities.

Being familiar with GatsbyJS, it's out-of-the-box SSR feature seemed like a good fit for the requirements. Alas, although there are plugins like gatsby-plugin-no-javascript, an issue that I ran into was I couldn't remove all JavaScript without the build breaking somewhere. Specifically, a polyfill.js was giving me some difficulty. I could have created a custom plugin, but the dependency overhead was too high given that the requirements were so basic and the majority of GatsbyJS features would not be used.

I took a step back and tried to find the simplest method that would yield the results I was looking for. I knew that I wanted to keep React, so the simplest approach would be to use ReactDOMServer.renderToStaticMarkup.

Integrating Styled Components

The next thing that needed addressing was styled-components. How do we extract the styles into a CSS file that is included in the page with a <link> tag?

styled-components has a code snippet for SSR: https://styled-components.com/docs/advanced#server-side-rendering. However, since we're using ReactDOMServer for rendering the page, there's no need to use Babel.

This is the example that was given:

import { renderToString } from 'react-dom/server'
import { ServerStyleSheet } from 'styled-components'

const sheet = new ServerStyleSheet()
const html = renderToString(sheet.collectStyles(<YourApp />))
const styleTags = sheet.getStyleTags()

The issue with this approach is that the HTML is rendered separately from the style tags, which means that the style tags need to somehow be inserted into the HTML string. Even in Styled Components' example dev server, they replace a HTML comment with the style tag.

An alternative to modifying the rendered HTML is to hard code the CSS file name and then render the Styled Components CSS to that file. There's also a good reason to use a file instead of inline CSS: caching. If the CSS file is moderately sized and doesn't change often, it might be beneficial to separate the CSS from the HTML. If the file does change, Etag Headers can be used to do some cache-busting.

Here's an example of a layout wrapper with the <link> tag that links to the CSS file.

const Layout: React.FunctionComponent = ({ children }) => (
  <html>
    <head>
      <meta charSet="utf-8" />
      <meta httpEquiv="x-ua-compatible" content="ie=edge" />
      <meta
        name="viewport"
        content="width=device-width, initial-scale=1, shrink-to-fit=no"
      />
      <link rel="stylesheet" href="style.css" />
    </head>
    <body>{children}</body>
  </html>
);

This layout component can be used to wrap the site's components.

const element = (
  <Layout>
    <MyComponent/>
  </Layout>
);

First, generate the HTML of the page. The ServerStylesheet.collectStyles function wraps a React element in a context provider that will be consumed by the useStyleSheet hook when styled components are created.

const elementWithCollectedStyles = serverStyleSheet.collectStyles(element);

Next, render to static HTML.

const html = ReactDOMServer.renderToStaticMarkup(
  elementWithCollectedStyles
);

Instead of using ServerStyleSheet.getStyleTags like the example, looking at the code of ServerStyleSheet, I opted to use ServerStyleSheet.instance.toString as it will output the same as getStyleTags but without the <style> tag.

const css = serverStyleSheet.instance.toString()

That's all there is to it...

Saving HTML and CSS

Next, the strings are written to their respective files. In this case, writeFile is from fs.promises in the NodeJS library.

const writeCssPromise = writeFile("./public/style.css", css);
const writeHtmlPromise = writeFile("./public/index.html", html);

Caveats

Although I did the rendering in one pass, there are at least two scenarios that would require two passes, or modification of the HTML string:

  • Inline the CSS in the HTML
  • Have a CSS filename that is a hash of the CSS content (so that the filename changes only when there's a change in the CSS)

Summary

I used React's ReactDOMServer.renderToStaticMarkup and Styled Components' ServerStyleSheet.collectStyles to render a completely static site with CSS and no JS.

Although it may seem strange to use a JS library to create a site with no JS, React makes it easy to encapsulate components and increase cohesion, therefore making codebases easier to work with. In addition, there are a vast selection of libraries that could help with making static sites. Some examples:

  • React Helmet for modifying meta tags and other SEO-related head tags
  • FormatJS for internationalization

The inspiration for this approach was specifically to generate pages that would work well as sources for <iframe> tags. Of course, there's no reason to just use this approach for iframes, the applications are limitless!

© 2022 Bayan Bennett