Tutorial: Build your own server side rendered (SSR) React app

Last updated on October 2022

I used to think that server side rendered react apps was quite complex to set up. And it is - behind the scenes. But we can use renderToString from react-dom/server and a basic express app to easily run our own SSR rendered React app.

This tutorial goes through a few steps to make use of react-dom/server to serve up the HTML of a basic React app. This is intended to be a quick and easy to follow tutorial for those who haven't set up ssr with react before.

First steps, install packages

We need to install a few dependencies first.

  • We'll use babel to transpile JSX to JS
  • nodemon to handle auto restarting our server
  • parcel to build the FE code
  • express to serve the rendered HTML (this will call the magic renderToString() function from react-dom/server to return the HTML. We will also use express to serve the built FE code (which was built with Parcel)
yarn add -D @babel/core @babel/preset-react @babel/register nodemon parcel 
yarn add express react react-dom 

Set up your React app

If you are looking to do SSR then you probably are familiar with React. I've created a simple app with 1 child component, which has a basic use of useState() so you can interact with a button.

Note: for this demo we'll stick with commonjs imports (require())

Your main App.jsx - put this and the child component in ./fe-app/App.jsx and ./fe-app/ClickCounter.jsx:

const React = require('react');
const ClickCounter = require('./ClickCounter');

const App = () =>
    (
        <div>
            <h1>Demo of SSR in React</h1>
            <p>Click the button to interact...</p>
            <ClickCounter/>
        </div>
    );

module.exports = App;

And the child ClickCounter.jsx

const React = require("react");

function ClickCounter() {
    const [count, setCount] = React.useState(0);

    return <button onClick={() => setCount(prev => prev + 1)}>
        You clicked {count} times
    </button>
}

module.exports = ClickCounter;

Now create a main client.js file (in the root directory) to render that main App component:

const React = require('react');
const {hydrateRoot} = require('react-dom/client');
const App = require('./fe-app/App.jsx');

hydrateRoot(document.getElementById('app-root'), <App/>);

Build your client side app

We will use Parcel to build the client side app.

Setup your package.json to look something like this. The important parts are the build script (and server script later on). Also make sure you don't have "main": "index.js" set!)

{
  "name": "a5h-ssr-react-demo",
  "version": "0.0.1",
  "license": "UNLICENSED",
  "scripts": {
    "build": "parcel build --no-source-maps --dist-dir dist client.js ",
    "clear": "parcel cache clear",
    "server": "nodemon --ext js,jsx server.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@babel/core": "^7.20.12",
    "@babel/preset-react": "^7.18.6",
    "@babel/register": "^7.18.9",
    "nodemon": "^2.0.20",
    "parcel": "^2.8.3"
  }
}

Now you can run yarn build and you should generate a index.js in your ./dist directory.

Note: you have to run yarn build before running the server (next section)

Server side rendering

As you probably guessed when looking at the package.json, we are running a server with express...

The thing that took the longest when writing this was the inital babel config (first 3 lines).

We import our react app (App.jsx), and call it with renderToString(createElement(App)) to get HTML.

Then we return that HTML on http://localhost:8080/ (and we include a <script src="app.js"> for the FE browser)

We also serve up the file from ./dist/index.js on http://localhost:8080/app.js.

Paste this in to a new file called ./server.js:

require("@babel/register")({
    presets: ["@babel/preset-react"],
});
const App = require("./fe-app/App.jsx");
const express = require("express");
const path = require('path');
const {createElement} = require("react");
const {renderToString} = require("react-dom/server");

const SERVER_PORT = 8080;
const app = express();

// Serve some HTML which has the rendered HTML for our React app,
// and a script tag to include the JS in the FE.
app.get('/', (_req, res) => {
    
    const html = `<html>
    <div id="app-root">${renderToString(createElement(App))}</div>

    <!-- app.js comes from your ./dist/index.js 
         If this is not there, run yarn build! -->
    <script src="app.js"></script>
</html>`;
    
    res.send(html);
});

// serve up the bundled/compiled React app
app.get('/app.js', (_req, res) => {
    res.set('Content-Type', 'application/javascript');
    
    const fileName = 'client.js';
    const filePath = path.join(__dirname, 'dist', fileName);

    res.sendFile(filePath);
})

console.log('Running server now. Remember to run `yarn build` to build the JS in ./dist directory');

app.listen(
    SERVER_PORT, 
    () => console.log(`Server running on http://localhost:${SERVER_PORT}`)
);

Once done, visit http://localhost:8080 and you should see an interactive app. View the source to confirm you see the HTML of our app!

And that is the end of this tutorial on setting up your first server side rendered React app.

Demo of the app (note: this is rendered through gatsby, not the express app shown above)

Click the button to interact...

© 2019-2023 a5h.dev.
All Rights Reserved. Use information found on my site at your own risk.