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.
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 tagsFormatJS
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!