WittCode💻

Server Side Rendering React with Express

By

Learn how to use server side rendering (SSR) with a custom built React application and Express. The React application is built from scratch with Webpack and has multiple routes with React Router. We will also learn what SSR is and the benefits of SSR.

Table of Contents 📖

What is Server Side Rendering?

Server side rendering, or SSR, is a technique that renders a single page application's (SPA's) HTML on the server as opposed to waiting for the browser to load and render it. Specifically, when it comes to React, the server program will import the React application's root component and render it into an HTML document to return to the client.

Why use Server Side Rendering?

SSR is used because it improves an SPA's search engine optimization (SEO) and performance. This is because rendering the page on the server means that search engine crawlers don't have to run any code in the browser to get to the rendered HTML. Instead, the server renders the HTML.

Project Structure Creation and Initialization

To begin, lets create a directory to hold our project called server-side-rendering-react-with-express.

mkdir server-side-rendering-react-with-express
cd server-side-rendering-react-with-express

Now lets initialize this project as an ES6 npm project with npm init es6 -y.

npm init es6 -y

Create a Custom React Application with Webpack

Now lets create our React application. To do this, we will use the module bundler Webpack. First, create a webpack configuration file called webpack.config.cjs. We use the .cjs extension because this file will use require and module.exports while our project uses ECMAScript import/export.

touch webpack.config.cjs

Now we need to install a few libraries to get this working. These are webpack itself, webpack-cli, the html webpack plugin, and some babel libraries. Install them as development dependencies by tagging on -D.

npm i -D @babel/preset-env @babel/preset-react babel-loader webpack webpack-cli html-webpack-plugin

The Babel libraries allow Webpack to convert our JSX code into code the browser understands. The html-webpack-plugin is a Webpack plugin that allows us to add our JavaScript code to a HTML file. Now lets configure webpack with our webpack configuration file. We will have two configurations, one for our Express server and one for our React application. To begin, lets fill in what both configurations share.

const path = require('path');
const webpack = require('webpack');
const htmlWebpackPlugin = require('html-webpack-plugin');

/**
 * Load JS and JSX files through Babel
 */
const babelLoader = {
  rules: [
    {
      test: /.(js|jsx)$/,
      exclude: /node_modules/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env',
            ['@babel/preset-react', {'runtime': 'automatic'}]]
        }
      }
    }
  ]
};

const resolve = {
  extensions: ['.js', '.jsx']
};

Both our Express server and React application will need to use Babel to convert JSX code into code that the browser understands. Both configurations will also resolve both JS and JSX extensions. This will allow us to leave off the .js and .jsx extensions when importing files. Now lets fill out our server configuration.

const serverConfig = {
  target: 'node',
  mode: 'development',
  entry: './src/server/server.jsx',
  output: {
    path: path.join(__dirname, '/dist'),
    filename: 'server.cjs',
  },
  module: babelLoader,
  plugins: [
    new webpack.EnvironmentPlugin({
      PORT: 3001
    })
  ],
  resolve
};

Here we are telling Webpack that our Express server is in a Node environment, it uses an environment variable called PORT, and it should start the bundling process using the server.jsx file and output it to a folder called dist under the name server.cjs. Now lets fill in our React application configuration.

const clientConfig = {
  target: 'web',
  mode: 'development',
  entry: './src/client/index.jsx',
  output: {
    path: path.join(__dirname, '/dist'),
     /*
      * Appends /static to index.html when looking for client.js
      * This is where Express is serving static files from
      */
    publicPath: '/static',
    filename: 'client.js',
  },
  module: babelLoader,
  plugins: [
    new htmlWebpackPlugin({
      template: `${__dirname}/src/client/index.html`
    }),
  ],
  resolve
};

Here we are telling Webpack that our React application is in a browser environment. We also tell it to output our bundle to a HTML file called index.html inside our src/client folder. The bundling process starts at src/client/index.jsx and outputs the result to a folder called dist with the name client.js. The publicPath key is important as it is where Express will be serving this application from. We will see this later on. Now we just need to export our configurations.

module.exports = [serverConfig, clientConfig];

Now lets create some useful commands inside package.json to run Webpack.

"scripts": {
  "clean": "rm -rf ./dist",
  "build": "npm run clean && webpack --config webpack.config.cjs",
  "start": "npm run build && node ./dist/server.cjs"
},

Now running npm start will clean out our dist folder, build a new dist folder with our webpack configuration, and then run our server application. Note that these commands might have to be a bit different on a Windows machine.

Creating the React Application

Now lets start working with React. First lets install react, react-dom, and react-router-dom from npm.

npm i react react-router-dom react-dom

Now lets create our src folder to hold our application code and then a client folder inside it to hold our React application.

mkdir src
cd src
mkdir client
cd client

Now lets create our index.html file to hold our React application.

touch index.html

Now lets fill in this index.html file.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SSR</title>
</head>
<body>
    <div id="root"></div>
</body>
</html>

This HTML file has some simple boiler plate code and a div element with the id root. This div element will hold our React application. Now lets create our index.jsx file.

touch index.jsx

This file will use the react-dom npm package's client API to initialize the app on the client.

import React from 'react';
import {hydrateRoot} from 'react-dom/client';
import {BrowserRouter} from 'react-router-dom';
import App from './components/App';

hydrateRoot(document.getElementById('root'),
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>);

The most important line in this file is hydrateRoot. The hydrateRoot method is used to display React components in a browser whose HTML was previously generated by react-dom/server. Specifically, the hydrateRoot method will attach each component's logic to the server generated HTML. In other words, hydrateRoot adds React functionality to the static HTML that the server generates. If we did not do this, then our application would simply be plain HTML returned from the server. The first argument to hydrateRoot is the DOM node that was rendered as the root element on the server. The second argument is the React node that we want to render the existing HTML of. This existing HTML will come from our Express server. We also surround our React application with React Router's BrowserRouter. The BrowserRouter enables navigation between components. Now lets create our App component in a folder called components.

mkdir components
cd components
touch App.jsx

Now lets fill in our App component.

import {Routes, Route} from 'react-router-dom';
import Home from './Home';
import About from './About';
import Navbar from './Navbar';

const App = () => {
  return (
    <main>
      <Navbar />
      <Routes>
        <Route path="/" element={ <Home/> } />
        <Route path="/about" element={ <About/> } />
      </Routes>
    </main>
  );
};

export default App;

Our App component has a navigation bar and two routes. One goes to the home page at / and the other goes to the about page at /about. Lets first create our Navbar.

touch Navbar.jsx

Now lets fill in our Navbar component.

import {Link} from 'react-router-dom';

function Navbar() {
  return (
    <div>
      <Link to="/">Home</Link>
      <Link to="/about">About</Link>
    </div>
  );
}

export default Navbar;

The Navbar component contains two Link elements that take us to the home page and about page. Now lets create the home component.

touch Home.jsx

Now lets fill in our Home component.

const Home = () => {
  return (
    <section>
      <h1>This is the home page</h1>
      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit,
        sed do eiusmod tempor incididunt ut	labore et	dolore magna
        aliqua. Ut enim ad minim veniam, quis nostrud exercitation
        ullamco laboris nisi ut aliquip ex ea commodo consequat.
        Duis aute irure dolor in reprehenderit in voluptate velit
        esse cillum dolore eu fugiat nulla pariatur. Excepteur
        sint occaecat cupidatat non proident, sunt in culpa qui
        officia deserunt mollit anim id est laborum.</p>
    </section>
  );
};

export default Home;

Our Home component just has an h1 tag letting us know we're on the home page and then some good old lorem ipsum. Lets create the About page now.

touch About.jsx

Now lets fill in our About component.

const About = () => {
  return (
    <section>
      <h1>About Page</h1>
      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit,
        sed do eiusmod tempor incididunt ut	labore et	dolore magna
        aliqua. Ut enim ad minim veniam, quis nostrud exercitation
        ullamco laboris nisi ut aliquip ex ea commodo consequat.
        Duis aute irure dolor in reprehenderit in voluptate velit
        esse cillum dolore eu fugiat nulla pariatur. Excepteur
        sint occaecat cupidatat non proident, sunt in culpa qui
        officia deserunt mollit anim id est laborum.</p>
    </section>
  );
};

export default About;

Our About component just has an h1 tag letting us know we're on the about page and then some more lorem ipsum, just like the home page.

Creating the Express Server

Now lets start working with Express. To begin, lets install Express from npm.

npm i express

Now lets create a server folder inside the src directory to hold our Express code.

mkdir server
cd server

Now lets create a file called server.jsx to hold our Express code.

touch server.jsx

This file has a JSX extension as it will import some JSX code. Namely, our App.jsx component. To begin, lets set up our Express server.

import express from 'express';

const app = express();
app.use('/static', express.static(__dirname));
const PORT = process.env.PORT;

app.get('*', async (req, res) => {
  res.status(200).send('hi');
});

app.listen(PORT, () => {
  console.log(`Server started on port ${PORT}`);
});

Here, we setup a Express server that listens on the port number provided by Webpack. It also serves static content from the route /static using the Express middleware function express.static. The static content that it serves up will be the files in our webpack generated dist folder. We also made it so our application listens for GET requests to any route and then sends back a 200 OK status code with the message hi. Now lets start importing the necessary packages to render our React application.

import {StaticRouter} from 'react-router-dom/server';
import ReactDOMServer from 'react-dom/server';
import App from '../client/components/App';
import fs from 'fs';

First we need to import StaticRouter from react-router-dom. The StaticRouter is used to render a React Router application in a server environment. We then import the react-dom/server API which we will use to render the React application on the server. We then import our App component of our React application and the core fs module. Now lest create a function that renders our React application.

/**
 * Produces the initial non-interactive HTML output of React
 * components. The hydrateRoot method is called on the client
 * to make this HTML interactive.
 * @param {string} location
 * @return {string}
 */
const createReactApp = async (location) => {
  const reactApp = ReactDOMServer.renderToString(
    <StaticRouter location={location}>
      <App />
    </StaticRouter>
  );
  const html = await fs.promises.readFile(`${__dirname}/index.html`, 'utf-8');
  const reactHtml = html.replace(
    '<div id="root"></div>', `<div id="root">${reactApp}</div>`);
  return reactHtml;
};

This function simply renders our React application to a String, then reads from our index.html file in the dist folder, places the React application HTML inside, and then returns the HTML. This function also takes a location as an argument. This location will come in from the request and is then passed to the StaticRouter. This is so if a user types in /about in the url bar it will render and return that page of the React application. Now lets add this function to our Express route.

app.get('*', async (req, res) => {
  const indexHtml = await createReactApp(req.url);
  res.status(200).send(indexHtml);
});

Now whenever we receive a GET request we will respond with the HTML of our rendered React application. Notice how we past the request url to the createReactApp function. This is so we render and return the desired page.

Running the Application

Now lets work with our application. To get it started, run npm start in the console.

npm start
...
webpack 5.88.1 compiled successfully in 828 ms
Server started on port 3001

When we run this command we can see some useful Webpack information and then the port that our server is running on. Now lets use the browser to navigate to localhost port 3001.

Image

From looking at the console network tab we can see how SSR was used. Specifically, we see the full HTML of the document in the response from GET requests to localhost and localhost/about. If this wasn't SSR we would simply see our div with the id root. However, as the React application was rendered on the server we see the fully rendered HTML. Something else that is important to note here is that this is simply static HTML that was returned from the server. It has no functionality. It is the client.js file that is retrieved from /static/client.js that adds the functionality to the website as it hydrates the HTML with the hydrateRoot function.