DEV Community

Cover image for Basics of React server-side rendering with Express.js
Juhana Jauhiainen
Juhana Jauhiainen

Posted on • Updated on

Basics of React server-side rendering with Express.js

If you want to develop SEO friendly and fast websites with React, you have two choices: server-side rendering (SSR) or static site generation (SSG).

There are some awesome frameworks like remix.run , next.js, astro or 11ty, which allow you to use one of (or both) techniques. So if you're building a production app, I recommend using one of them because server-side rendering is quite hard to get right.

But if you want to understand how it works and what is happening under the hood in these frameworks, you definately should try it out. This article will focus on how SSR works and we will also go through a simple example of using SSR.

How React server-side rendering works?

Server-side rendering means rendering the initial HTML on the server instead of waiting for the JavaScript to be loaded in the browser and then rendering.

In client-side rendering, the browser makes a request for the index.html page, the server responds. the browser then reads this .html file and makes requests for any additional resources defined in it (CSS, JavaScript, favicon, images etc.). Only once the JavaScript is downloaded and can be executed, will there be anything rendered on the screen.

As we can see, the server doesn't really do anything here. That's why you can host a client-side rendered React app by just serving the static files using a web server like nginx.

Client-side rendering

With server-side rendering, you need a server side application which handles the initial rendering of your React application. The server application will import your React applications root component and render it into a HTML document which is then returned to the client.

server-side rendering

Do I need to use server-side rendering in my React app?

If you're starting a new project and are serious about performance and SEO you should definately look into SSR. I'd recommend using one of the React frameworks tailored for SSR if they fit your needs.

For existing client-side rendered apps you should really weight the pros and cons. While SSR might provide some benefits (SEO, loading speed, social media previews), it will cost you some development time and will increase your server costs.

How to implement server-side rendering

We're going to go through a simple, but limited, implementation of server-side rendering just to get you an idea on how it works.

You can use any Node.js or Deno framework for the server code but in this example, we're using Node.js, express and esbuild. The full source code of this example can be found here

First let's look at the client side code.

Our main code in the client side is in App.jsx.

import * as React from "react";

export default function App() {
  const [times, setTimes] = React.useState(0);
  return (
    <div>
      <h1>Hello {times}</h1>
      <button onClick={() => setTimes((times) => times + 1)}>ADD</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

App.jsx contains a small React component with a counter which is increased when the user clicks a button. The only other file in the client side we need is an entrypoint.

import * as React from "react";
import ReactDOM from "react-dom";
import App from "./App";

ReactDOM.hydrate(<App />, document.getElementById("root"));
Enter fullscreen mode Exit fullscreen mode

index.jsx is the entrypoint for our client side code. Notice we're using ReactDOM.hydrate instead of ReactDOM.render. Instead of rendering the app (because it has already been rendered by the server) we are hydrating our app.

Hydrating refers to attaching all the event handlers to the correct elements of our server-side rendered DOM so our application will function correctly.

Next, let's take a look at the server code.

import path from "path";
import fs from "fs";

import React from "react";
import ReactDOMServer from "react-dom/server";
import express from "express";

import App from "../src/App";

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

app.get("/", (req, res) => {
  fs.readFile(path.resolve("./public/index.html"), "utf8", (err, data) => {
    if (err) {
      console.error(err);
      return res.status(500).send("An error occurred");
    }

    return res.send(
      data.replace(
        '<div id="root"></div>',
        `<div id="root">${ReactDOMServer.renderToString(<App />)}</div>`
      )
    );
  });
});

app.use(
  express.static(path.resolve(__dirname, ".", "dist"), { maxAge: "30d" })
);

app.listen(PORT, () => {
  console.log(`Server is listening on port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

On the server side, we use express to define a root endpoint which serves a index.html file. When a request is received, we render our React app root component App to a string using ReactDOMServer.renderToString. The rendered string is then injected into our index.html file so that we replace the div with the id root with our rendered content.

We also setup static file loading from dist folder so that our client side JavaScript code in bundle.js will be loaded once the browser reads index.html.

index.html contains basic HTML structure, a <script> tag for bundle.js and a <div> element which the React app will be rendered to.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>SSR App</title>
  </head>

  <body>
    <div id="root"></div>
    <script src="bundle.js"></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Now, when a request is made to the root of our app, the express server renders our React app into a string and injects that into the HTML which is returned to the browser. The browser then loads our JavaScript file (bundle.js) which contains the ReactDOM.hydrate call. After hydrate is called, our application is fully interactive and works just like it before we moved to server-side rendering.

This setup is enough for a simple example, but falls down pretty quickly with a more complext app. For example, it has no support for routing, which means we would render the same HTML no matter which URL the user is loading. It's also missing setup for loading static files imported in React components.

Summary

Server-side rendering is a useful technique you can use when you want to improve the load times and SEO of your React application. It is however, hard to implement well and might not be needed if your client-side rendered application is performing well and you don't have issues with SEO.

I strongly recommend you to try remix or next.js if you want to build a server-side rendered app.

Links

React docs on ReactDOMServer
React docs on hydrate
remix.run
next.js
Is SSR with React worth it? (Jakob Lind)

Photo by Sean Sinclair on Unsplash

Oldest comments (6)

Collapse
 
ojpro profile image
Oussama ELJabbari

Thanks, that is a useful article.

{Note} You should update the nextjs url.

Collapse
 
juhanakristian profile image
Juhana Jauhiainen

Thanks for pointing that out 👍

Collapse
 
jaxramus profile image
jaxramus

this doesn't cover more than 1 route

every single article i see for express + react SSR, they only cover the most basic example and don't show you how to hydrate different components for different routes

Collapse
 
thebearingedge profile image
Tim Davis

You'd need route matching in your app with something like react router.

Collapse
 
fcojavier0099 profile image
FcoJavier0099

Greetings, great information!, thank you so much.

I want to contribute with this observation:

  • The code didn't work for me at first time, the button didn't work because the page couldn't load "bundle.js"
  • I solved the problem adding the static middleware:
...
const app = express();

// I added this line
app.use(express.static(path.join(__dirname,'..','dist')))

app.get("/", (req, res) => {
... 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
audreybozorgi profile image
Audrey

There are lots of things not covered in this code snipets. Ill mention them after I done it.