React Server side rendering (SSR) for existing projects

Frikky
The Startup

--

React has been a good option for the past few years, mainly due to create-react-app, the ease of use, and the “no-bullshit” configuration. Over time as I’ve discovered and grasped new React concepts better, a new one always keeps popping up, with server side rendering (SSR) being the current iteration.

Having been in infrastructure and built products a plenty, (both frontend and backend) I thought this would be a breeze. Boy was I wrong. It ended in infuriating battles with tools I didn’t want to understand, configuration files I didn’t want to touch and problems I REALLY didn’t want to solve. In time though, the “go to sleep and wake up with a solution”-tactic worked pretty well for clearness and understanding. We now server side render important dynamic pages for thousands and thousands of API-discovery pages for our customers at Shuffle.

Why serverside render?

There are a lot of blogposts about this already, talking about anything from performance to analytics, but NONE of them seem to start with an existing userbase. My own point was SEO and to make it readable to Google-bot and the like, along with making dynamic links work.

You should NOT server-side render if you intend to only use dynamic content that is private to individual users. You SHOULD server-side render if you intend to make public dynamic content easily discoverable, such as documentation pages.

Why does it have to be so hard..?

I’m not building a website from scratch. I already have one. Blogposts about how to do it from scratch are easy to find, but none really tell you the intricacies of “converting” an existing project. They might be from 2017 or earlier as well, which makes them obsolete at this point.

First, here are my notes from going through the process (I have more scribbles too). These are good to have to reflect on the hard parts and “aha” moments.

2020 is not 2017Fml, this sucks.
* Babel — wtf and why? — Convert from ES.. to commonjs
* Webpack — WHY?? — Make it a single thing
* ReactJS 16.7 (hooks)
* ES2015, CommonJS
* Express..
* Architecture?
* Why not Go?
Hydration..?
WHEN ALL WORKS:
* Overrides - document, window, globals
* Fitting it together - src/client, src/server, function/
* Putting it in a function
* Routing?
* STYLES!!!
Nextjs? Gatsby?
* Be wary of window, document (and workarounds), no JSDOM — gatsby blog
* Virtual browser memory
* Fixing the view once you finally got it working (material UI)
* (don’t finish everything server side)
* Device detection
* Webpack compression
Don’t forget the fucking viewport for mobile views

If you’re anything like me, you like jumping straight into things and read only when necessary. Here are the basics you NEED to know to make this understandable at all:

  • ES2015–2017..: A style of Javascript used by ReactJS (uses e.g. import)
  • CommonJS: The style of Javascript used by your browser (?)
  • Babel: Turns your ReactJS code (ES2015~) into CommonJS (transpile)
  • Webpack: Packs everything into a single js file that can be imported AFTER server side rendering is finished
  • Express: Just another webserver

From here on, we’ll look at some code and the changes that are required to make the leap, starting from create-react-app.

The move

My directory went from something like this (create-react-app):

src/index.js  # Default entrypoint for ReactJS
src/App.js # First component used from index.js
build/ # Produced by: $ npm run build (react-scripts build)
node_modules/ # Produced by: $ npm i
public/ # Has everything public in it
package.json # Contains all packages and scripts etc.

TO

src/index.js           # Default entrypoint for ReactJS CLIENT
src/client/App.js # First component used from src/index.js
src/server/index.js # Server entrypoint (ES2015~)
functions/index.js # File created by BABEL (SERVER entrypoint)
functions/src/ # File created by BABEL (CLIENT entrypoint)
functions/package.json # Packages used by server for SSR
functions/node_modules # Node modules used by server for SSR
public/bundle.js # Bundle generated by WEBPACKwebpack.config.js # WEBPACK configuration
.babelrc # BABEL configuration
package.json # Contains a few MORE scripts than before

node_modules/ # Produced by: $ npm i

As you can see, the following has changed:

  • src/ has become src/client/ (except for index.js, client entrypoint)
  • src/ has a new folder “server”.
  • functions/ appeared, containing the server side data. PS: None of the code in this folder is manually written, just generated from src/ with Babel. The functions/src folder makes it possible for the SERVER to use code from the CLIENT, which is why the server has to be written in the same language as the client.
  • We have a webpack configuration and bundle
  • We have a .babelrc
  • The build/ folder is gone

The configuration

If you have an existing project already, go to the folder, but in case you don’t:

npx create-react-app ssr-demo
cd ssr-demo

1. Dependencies (production & development)

npm install @material-ui/core express react-router-dom 
npm install @babel/cli @babel/core @babel/preset-react @babel/preset-flow babel-loader babel-preset-env webpack-cli webpack --save-dev # Developer dependencies

2. Configuration files

(yes, I hate these as much as the next person) — .babelrc (webpack comes when we got this working).

Create a .babelrc file and add this:

Babel configuration

Run Babel from the local node_modules directory to test transpilation (is that a real word?):

node_modules/.bin/babel ./src -d functions/

Set up package.json for our SERVER (to make this easy, its the same as client) in the functions/ folder.

cp -r package.json node_modules/ functions/

The Code

  • Move everything from ./src into ./src/client and make a ./src/server directory
mkdir ./src/client ./src/server
mv ./src/* client
  • Create basic server code with express. We will create this with the same style as React (import vs. require), before transpiling it with babel to commonJS, used by our node server. Before this one works properly, we need to also transpile our client code (because it imports from ./src/App).

./src/server/index.js

import React from 'react'import App from './src/App'import { renderToString } from 'react-dom/server'
import { StaticRouter } from 'react-router-dom';
import { ServerStyleSheets, ThemeProvider } from '@material-ui/core/styles';
import express from 'express'
// Same as our index.html. Replace data in it
// meta = meta data you can send yourself
// css = generated from e.g. material-ui
// html = generated from CLIENT code in ./src, e.g. under /functions/src AFTER its transpiled from src
function renderFullPage(meta, html, css) {
return `
<!DOCTYPE html>
<html>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link rel="icon" href="/favicon.ico" />
<link rel="manifest" href="/manifest.json" />
${meta}
<style id="jss-server-side">
${css}
body {
margin: 0;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</style>
</head>
<body>
<div id="root">${html}</div>
<script src="/bundle.js"></script>
</body>
</html>
`;
}
// Initialize app
const app = express()
//Used to serve e.g. bundle.js
app.use(express.static('public'))
// Create a wildcard route catch (all routes)
app.get('**', (req, res) => {
const sheets = new ServerStyleSheets();

const context = {};
const meta = `
<title>Hello this is meta</title>
`
// IF you have a theme, import it here as theme={theme}
const app = renderToString(
sheets.collect(
<ThemeProvider>
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
</ThemeProvider>
)
)
const css = sheets.toString();
const renderedData = renderFullPage(meta, app, css)
return res.send(renderedData)
})
// cloud function for server side
app.listen(3000, () => console.log(`Example app started!`))
  1. Transpile ALL your code (including CLIENT)

PS: you might find issues with e.g. css and svg files or images if they’re imports. As I used inline styles, and normal image referencing, this wasn’t an issue in our case.

node_modules/.bin/babel ./src/server -d functions/ && node_modules/.bin/babel ./src/client -d functions/src

2. TEST your server code locally

You will need Node, which you… should have at this point if you’re using npm, otherwise install it (e.g. $ sudo apt install nodejs).

node functions/index.js 

Now go in your browser at http://localhost:3000 and you should see a basic outline of your app. There might be warnings, but that’s normal at this point (e.g. missing files). If this does not work, then keep configuring the Server’s index.js file, as this is the start point for everything.

3. (The dreaded) Webpack

Did you see the line in ./src/server/index.js that imported the script “/bundle.js”? This is the file that will CONTINUE execution of javascript after the initial server render is done, also called Hydration. The request scheme looks something like this:

1st request: Server side rendered (node ./functions/index.js)

2nd->n’th request: Client side rendered (bundle.js)

To make this possible, we need to use webpack, which puts all your client code into a single file.

webpack.config.js

module.exports = {
entry: {
"app": "./src/client/index.js",
},
module: {
rules: [
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
exclude: /node_modules/,
},
{
test: /\.js$/,
loader: "babel-loader",
exclude: /node_modules/,
}
],
},
output: {
path: __dirname+"/functions/public",
filename: "bundle.js",
},
}

Here’s how it works:

entry: Where to start loading client code?

modules: How do we handle the code? In our case, we use .babelrc which contains a bunch of React specific configurations.

output: The bundle.js file we talked about briefly. This should have all our code, but might be big as we don’t really do any optimizations here.

Building:

node_modules/.bin/webpack-cli --config webpack.config.js

Testing:

Open your browser and go to http://localhost:3000/bundle.js (with the node server running). Open the developer tools (f12), and click “network”. Find whether it loads “/bundle.js”. Our webpack configuration puts the bundle in ./functions/public/bundle.js, which makes it available to node.

Real life

It feels kind of easy to write a post like this after the fact, but I spent 3 full days getting this to work in a real world environment on Firebase. Don’t get discouraged. There are A LOT of things that can break with this basic setup, and it’s REALLY confusing when you first get started. Here are just a few normal issues:

  • Styles & images (e.g. svg’s, stylesheets and material-UI)
  • Routing (server vs client side)
  • Virtual DOM vs Browser DOM
  • Globals (document, window…)
  • Device detection
  • Browser history
  • Why not NextJS and/or Gatsby?
  • Use with firebase & serverless things
  • Development tricks

I’ll get into these in the next one, but thought I should write this out now that I’ve gotten it to work pretty well. Get in touch if you want to hear more before I get that far.

If you want to read about serverless, GCP, frontend or backend things, cloud functions or security, check out my other posts here

Have a good one, and stay safe :)

--

--