logosmonn
Cover photo by Christopher Burns

Minimal server-side rendering with React, part 1

Published by Simon Ingeson

This is part of a series:

  1. Initial setup with @babel/*, react, and express (current post)
  2. Configure nodemon and react-router
  3. Setup production build and benchmarking

First, let’s cover some caveats:

  1. While technically possible, this is not meant for a production setup. It’s just a way to demonstrate server-side rendering with React.
  2. It won’t generate a browser bundle. That means there is no hydration step as no JavaScript will execute in the browser. The upside with this is that the app will be faster.
  3. This blog post assumes knowledge of modern JavaScript and React.

Let’s get started.

Setup the project

Feel free to adjust these steps according to your needs. For example, swap to yarn instead of npm if that’s your thing. Open up your favorite terminal and run these commands:

mkdir basic-ssr
cd basic-ssr
npm init --yes # create package.json using defaults
npx gitignore node # loads basic .gitignore for Node.js projects

Install dependencies

Again, back in your terminal:

npm install \
  express \
  react \
  react-dom \
  @babel/core \
  @babel/register \
  @babel/preset-env \
  @babel/preset-react

Usually, you would install the @babel/* libraries as devDependencies. However, since this setup will not have a build step, they can be included as regular dependencies.

You can easily replace express with something else, say fastify.

Setup start script

Edit the package.json file:

  "scripts": {
-   "test": "echo \"Error: no test specified\" && exit 1"
+   "start": "node --require @babel/register index.js"
  },

Note the --require flag for @babel/register. That’s what lets us avoid a build step. Another option would be to use @babel/node.

Add .babelrc in the project root folder

Set "runtime": "automatic" to avoid having to import React.

{
  "presets": [
    "@babel/preset-env",
    ["@babel/preset-react", { "runtime": "automatic" }]
  ]
}

Add index.js in the project root folder

import express from 'express'
import { renderToString } from 'react-dom/server'

const app = express()

function App() {
  // To avoid putting all of this in a string, we'll use JSX for html, head, body, etc. too.
  return (
    <html lang="en">
      <head>
        <meta charSet="UTF-8" />
        <meta name="viewport" content="width=device-width" />
        <meta name="description" content="Hello World" />
        <title>Hello World</title>
      </head>
      <body>
        <h1>Hello World</h1>
      </body>
    </html>
  )
}

app.get('/', (_, res) => {
  // Since we don't care about hydration here, we can safely remove the `data-reactroot` attribute.
  const markup = renderToString(<App />).replace(` data-reactroot=""`, '')
  // We need to set the doctype here as we can't include it using JSX.
  res.send(`<!DOCTYPE html>${markup}`)
})

app.listen(process.env.PORT || 3000)

And that’s it! You can run this with npm start. In future blog posts, I will improve on this project and move towards a more production-friendly setup. You can find the source code in the GitHub repository.

Cover photo by Christopher Burns.