WittCode💻

Server Side Rendering React MUI with Express

By

Learn how to use server side rendering (SSR) with a custom built React MUI application and Express. The React application is built from scratch with Webpack and has a custom theme created with React MUI. 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 a 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.

Project Structure Creation and Initialization

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

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

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

npm init es6 -y

Create React Application From Scratch 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.js.

touch webpack.config.js

Now we need to install a few libraries to get this working. First lets install Webpack itself and webpack-cli as development dependencies.

npm i webpack webpack-cli -D

Webpack will transpile our JSX code to JavaScript that the browser understands. However, for Webpack to do this we need to install a loader for webpack called babel loader, along with some other Babel dependencies.

npm i @babel/preset-env @babel/preset-react babel-loader -D

A loader is simply a function that Webpack passes code through to perform some sort of transformation. We also want to install a plugin for Webpack called html-webpack-plugin.

npm i html-webpack-plugin -D

A Webpack plugin interacts with the Webpack lifecycle. The html-webpack-plugin creates an HTML file to place our bundled JavaScript code into. Now that we have the required libraries, lets configure webpack with a 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.

import path from 'path';
import webpack from 'webpack';
import htmlWebpackPlugin from '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'}]
          ]
        }
      },
      // By default this is true, setting to false means
      // we don't have to specify the extension for .mjs
      // files or packages where package.json type is module.
      resolve: {
        fullySpecified: false
      }
    }
  ]
};

/**
 * Webpack needs to know to look for JSX files
 */
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. Now lets fill out our server configuration.

const serverConfig = {
  target: 'node',
  mode: 'production',
  entry: './src/server.jsx',
  output: {
    path: path.resolve('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. We use the .cjs extension because this file will contain CommonJS require and exports. Now lets fill in our client/React application configuration.

const clientConfig = {
  target: 'web',
  mode: 'production',
  entry: './src/client/index.jsx',
  output: {
    path: path.resolve('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: path.resolve('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.

export default [serverConfig, clientConfig];

Creating Useful package.json Scripts

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.js",
  "start": "npm run build && node ./dist/server.cjs"
},

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.

Installing React MUI

Now lets install React MUI. Specifically, we will be using Material UI which is a React component library. To get started, we need to install a few libraries to get Material UI to work. These are @mui/material, @emotion/react, and @emotion/styled.

npm i @mui/material @emotion/react @emotion/styled

@mui/material is a library of components, @emotion/react adds styling in React, and @emotion/styled makes it easy to create styled components with JavaScript. Emotion is a library designed for writing CSS with JavaScript.

Creating the React Application

Now lets start working with React. We will create a simple React application that uses a custom Christmas themed style created with React MUI. To get started, install react and react-dom from npm.

npm i react react-dom

Now lets create a client directory to hold our React application and a components directory inside it to hold our React components.

mkdir client
cd client
mkdir components

Now lets create an index.html file inside our client directory. This file will be the template that Webpack uses to place our React application inside.

touch index.html
<!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>
    <style></style>
</head>
<body>
    <div id="root"></div>
</body>
</html>

The empty style tags will be replaced with our server rendered CSS from React MUI. The div element with the id root will be replaced with our server rendered HTML. Now before we continue, we should create a file called createEmotionCache.js at the top level of our src directory.

touch createEmotionCache.js

This file will export an emotion cache that is shared among the client and server. An emotion cache is a way to configure how emotion inserts styles. With each request our Express server receives, we will create a new emotion cache. This cache will be shared among the client and server. We create this cache using the library @emotion/cache. Install it from npm.

npm i @emotion/cache

Inside our createEmotionCache.js file lets import the @emotion/cache library and create a cache.

import createCache from '@emotion/cache';

const WITTCODE = 'wittcode';

export default () => {
  return createCache({key: WITTCODE});
};

The createCache function is how we configure emotion to insert styles. Specifically, this will create a new Emotion cache to extract the styles needed by the server rendered HTML. The key argument will be the value on the style tags that emotion inserts. Now lets create an index.jsx file to link our DOM to React.

touch index.jsx

First, lets import everything this file will use.

import React from 'react';
import {hydrateRoot} from 'react-dom/client';
import App from './components/App';
import {ThemeProvider} from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import christmasTheme from './themes/christmasTheme';
import {CacheProvider} from '@emotion/react';
import createEmotionCache from './createEmotionCache';

Now, before we go any further, lets create the christmasTheme that we are importing. This will be in a directory called themes at the top level of our src directory.

mkdir themes
cd themes
touch christmasTheme.js

Lets first import the createTheme function from MUI.

import {createTheme} from '@mui/material/styles';

This function allows us to create our own custom theme, which in this instance, is a christmas theme. Lets create this theme.

const christmasTheme = createTheme({
  palette: {
    background: {
      default: '#165B33',
    },
    primary: {
      main: '#BB2528',
    },
    secondary: {
      main: '#ECECEE'
    }
  },
  typography: {
    fontFamily: 'cursive',
    fontSize: 14,
  },
});

export default christmasTheme;

Now when we wrap our React MUI application with this theme it will have a Christmasy green background, red primary color, and white secondary color. We also set the default font to be cursive. Now lets also create the App component that our index.jsx file is importing.

cd components
touch App.jsx

Our App.jsx file will be very simple, just some lorum ipsum in a Typography element that uses our Christmas theme. A very beautiful application!

import Typography from '@mui/material/Typography';
import Stack from '@mui/material/Stack';
import {useTheme} from '@mui/material';

const App = () => {
  const theme = useTheme();

  return (
    <Stack>
      <Typography variant='h1' color={theme.palette.secondary.main}>
        WittCode's Blog
      </Typography>

      <Typography variant='body1' color={theme.palette.primary.main}>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dictum leo eu nulla aliquam, sed mollis turpis eleifend. Mauris dictum tellus mi, sed dictum lectus maximus in. Nulla posuere pellentesque lorem, et dapibus augue pulvinar vel. Nulla rhoncus augue purus, a interdum odio facilisis sed. In porta condimentum eros nec aliquet. Nullam luctus bibendum ante. Pellentesque varius, justo vestibulum maximus aliquet, lectus neque volutpat ligula, consequat ornare tellus est eget metus. Sed rutrum non odio vel viverra. Proin pharetra ante nec nisi pulvinar dignissim. Curabitur sit amet vestibulum ligula.
      Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec ut diam in turpis blandit cursus. Aenean et volutpat nisi. Vivamus et tortor odio. Praesent tristique luctus nulla, vel ultrices ipsum vehicula vel. Quisque tempor aliquam libero, a posuere lorem hendrerit et. In lectus urna, tincidunt in tempus vitae, fringilla vitae ante. In quam lacus, viverra sit amet volutpat eget, imperdiet eget eros. Sed a viverra tortor, ultrices placerat arcu. Aliquam ac tellus in est dictum maximus vel interdum lectus. Ut sed enim facilisis, dapibus eros eget, tincidunt urna. Phasellus et semper lacus, eget rhoncus tellus. Proin malesuada vulputate lacinia. Sed eu pharetra augue.
      Nunc ornare malesuada urna egestas fringilla. Donec in ullamcorper massa, ut finibus arcu. Curabitur laoreet luctus diam, convallis euismod quam consectetur gravida. Nam ligula turpis, maximus id aliquam eu, tincidunt a nisl. Donec elit diam, scelerisque ac elementum sed, mattis ut velit. Quisque at interdum ex. Sed euismod velit at orci rutrum, at dapibus eros ornare. Sed in turpis non est placerat euismod. Nullam eu enim fringilla, bibendum turpis sit amet, fringilla mauris. Donec eu augue tortor. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vivamus hendrerit justo nunc, laoreet interdum turpis ornare congue. Praesent id euismod leo, ac blandit magna. Nulla interdum lorem enim, vel dapibus erat auctor convallis. Donec tristique est quis dolor sagittis, id maximus neque elementum.
      Donec venenatis ornare urna eget cursus. Sed sit amet nisl aliquet, auctor nunc vitae, tincidunt turpis. Duis vitae facilisis mauris. Quisque varius semper luctus. Mauris at elementum felis. Fusce non mi tincidunt leo congue rhoncus. Mauris sit amet porta orci. Nam a suscipit turpis. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nam nibh ligula, porttitor a venenatis vitae, imperdiet ut elit. Praesent vitae sodales sapien. Pellentesque nec leo vitae tellus tristique vulputate.
      Praesent rhoncus condimentum turpis. Pellentesque mattis dolor eget augue finibus pretium. Nunc quis commodo tellus. Cras condimentum diam interdum enim venenatis malesuada eu in urna. Duis feugiat convallis nulla, sed faucibus lacus sollicitudin sit amet. Sed accumsan tellus orci, sed tincidunt erat condimentum et. Suspendisse ullamcorper libero ac lobortis dictum. In hac habitasse platea dictumst. Cras eget sagittis erat, id sagittis sapien. Nulla elementum massa nec velit mollis sollicitudin. Ut sit amet nunc a libero fermentum venenatis ut non felis. Integer quis euismod diam. Integer ut pulvinar risus, nec porttitor risus. In convallis ipsum non venenatis tincidunt. Suspendisse ullamcorper laoreet enim, et pretium urna congue at.
      </Typography>
    </Stack>
  );
};

export default App;

Now, back in our index.jsx file, lets hydrate our application. In SSR terms, hydrating means adding functionality to some plain HTML. In other words, when we render our React application on the server, it won't have any functionality. Clicking buttons wouldn't work, hooks wouldn't fire, etc. When our client.js file is served up from our Express server, it is executed in the browser and adds functionality to the plain HTML that it rendered. This is all handled by the hydrateRoot method.

const emotionCache = createEmotionCache();
  
/**
 * The hydrateRoot method attaches React to the HTML
 * generated on the server. Hydration turns the server
 * generated HTML into a fully interactive app that runs
 * in the browser.
 */
hydrateRoot(document.getElementById('root'),
  <React.StrictMode>
    <CacheProvider value={emotionCache}>
      <ThemeProvider theme={christmasTheme}>
        <CssBaseline />
        <App />
      </ThemeProvider>
    </CacheProvider>
  </React.StrictMode>
);

It is important that the application we are hydrating here is identical to the one on the server. If it isn't, React will fall back to the client rendered React application. Here, our application is wrapped by the Emotion cache and christmas theme. We also use the CssBaseline component to provide a base for MUI to build off of.

Rendering our React Material UI App on the Server

When using SSR with a Material UI application it is important to provide the page with the CSS as well as the HTML at the same time. If not, the CSS will come from the JavaScript file, which needs to be fetched from the server separately. Due to this delay, a flicker will appear as the styles are loaded. To combat this flicker, when we render the React application on the server, we pull the CSS out and pass it to the client. To start rendering on the server, we need to install the library @emotion/server.

npm i @emotion/server

The @emotion/server library allows us to render a Material UI application on the server by extracting CSS from the application. This library also requires @emotion/css to be installed.

npm i @emotion/css

The @emotion/css library provides a way to use emotion. Now inside server.jsx, import @emotion/server/create-instance and our createEmotionCache function.

import createEmotionServer from '@emotion/server/create-instance';
import createEmotionCache from './createEmotionCache';

The createEmotionServer uses our emotion cache to produce two methods, extractCriticalToChunks and constructStyleTagsFromChunks. Lets create a function that does this.

/**
*
* @param {Request} req
* @param {Response} res
*/
const renderReactApp = (req, res) => {
  const emotionCache = createEmotionCache();
  const {extractCriticalToChunks, constructStyleTagsFromChunks} =
    createEmotionServer(emotionCache);
};

This function will be passed to Express as middleware. For each request, we will create an emotion cache and then pass it to our emotion server. Next we need to render our React application to HTML. We can do this by importing a React DOM server from the react-dom library. We also need to import the MUI ThemeProvider, our actual theme, CssBaseline, @emotion/react CacheProvider, and our React application itself.

import ReactDOMServer from 'react-dom/server';
import {ThemeProvider} from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import christmasTheme from './themes/christmasTheme';
import {CacheProvider} from '@emotion/react';
import App from './client/components/App';
import React from 'react';

The CacheProvider provides our cache to the React application, ThemeProvider provides our theme, and the CssBaseline provides a CSS basis that MUI builds off of. Now lets render our React application to HTML. This can be done with the ReactDOMServer renderToString function.

const reactHtml = ReactDOMServer.renderToString(
  <React.StrictMode>
    <CacheProvider value={emotionCache}>
      <ThemeProvider theme={christmasTheme}>
        <CssBaseline />
        <App />
      </ThemeProvider>
    </CacheProvider>
  </React.StrictMode>
);

Now we can use our emotion server returned functions to extract the CSS from our HTML.

// Extract the CSS
const emotionChunks = extractCriticalToChunks(reactHtml);
const emotionCss = constructStyleTagsFromChunks(emotionChunks);

These two functions work together to extract the CSS from the rendered React application. Now lets apply our CSS and HTML to our template index.html file that Webpack makes for us. To make this easier, lets first import the core Node fs and path modules.

import fs from 'fs';
import path from 'path';

Now lets read the index.html file and place our CSS and HTML inside.

// Add CSS and HTML to template file
const indexHtml = await fs.promises.readFile(`${path.resolve('dist')}/index.html`, 'utf-8');
const renderedApp = indexHtml
  .replace('<style></style>', emotionCss)
  .replace('<div id="root"></div>', `<div id="root">${reactHtml}</div>`);

Here we simply replace our style tags with the emotion CSS and our root element with our React HTML. Now we just need to respond with the rendered application.

res.status(200).send(renderedApp);

Creating the Express Application

So we've got a middleware function created, but now lets get our Express application setup to use this middleware. First, lets install express from npm.

npm i express

Next, lets import express into our server.jsx file.

import express from 'express';

Now lets instantiate our Express app and create a variable to hold the port value that the server will listen on.

const app = express();
const PORT = process.env.PORT;

Now lets tell Express to serve up static content from the Webpack created dist folder at the route /static. Remember, we configured Webpack to look request static content from /static.

app.use('/static', express.static(path.resolve('dist')));

The dist folder is the folder that Webpack is configured to place all its output. Webpack will bundle all our code and then output it to a folder called dist. Now lets add our renderReactApp function as application level middleware and setup our server to listen for requests on the provided port number.

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

Now all requests to localhost port 3001 will use the renderReactApp middleware.

Project Demonstration

Now lets demonstrate the project in action. To get it running, navigate to the same directory as the package.json file and run npm start.

npm start

Image

Note how in the Gif we can see the HTML and CSS of the React application is rendered when a request is sent to localhost:3001. We can also see the client.js bundle being served from /static/client.js. It is this client.js file that hydrates our React application and makes it functional, but the HTML and CSS has already been rendered as seen in the response from localhost:3001. This will give us an SEO boost! Oh yeah!