How to Implement Server-Side Rendering in React

Server-Side Rendering (SSR) in React improves application load times, enhances user experience, and boosts search engine optimization (SEO) by rendering components into HTML on the server before sending them to the browser. This article provides a direct, step-by-step guide on how to manually implement SSR in a React application using Node.js, Express, and React’s built-in server APIs. You will learn the core architecture of SSR, how to render components on the server, and how to hydrate them on the client.


The Core Concept of React SSR

In a standard Client-Side Rendered (CSR) React app, the server sends a blank HTML file and a JavaScript bundle. The browser downloads the JS and renders the UI.

With SSR, the process shifts: 1. Server Rendering: The server executes the React code, renders the component tree into static HTML, and sends this pre-rendered HTML to the browser. 2. Immediate Paint: The user immediately sees the visual content of the page. 3. Hydration: The browser downloads the JavaScript bundle and attaches event listeners to the existing HTML, making the page fully interactive.


Step 1: Set Up the Client-Side Hydration

To transition from static HTML to an interactive React application, you must “hydrate” the HTML on the client. Instead of using createRoot, use hydrateRoot from react-dom/client.

Create your client entry point (client.js):

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

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

Step 2: Create the Express Server

To render React on the server, you need a Node.js server environment. Express is commonly used for this purpose.

Using the renderToString function from react-dom/server, you can compile your main React component into a static HTML string.

Create your server file (server.js):

import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './App';

const app = express();

// Serve static assets (like your compiled client-side JavaScript bundle)
app.use(express.static('public'));

app.get('*', (req, res) => {
  // Render the React component to an HTML string
  const appMarkup = renderToString(<App />);

  // Inject the rendered markup into a full HTML template
  const html = `
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>React SSR App</title>
      </head>
      <body>
        <div id="root">${appMarkup}</div>
        <script src="/bundle.js"></script>
      </body>
    </html>
  `;

  // Send the complete HTML page to the client
  res.send(html);
});

app.listen(3000, () => {
  console.log('Server is running on http://localhost:3000');
});

Step 3: Configure Build Tools (Babel and Webpack)

Node.js does not natively understand JSX or ES modules out of the box. You must configure Webpack and Babel to compile both the server-side and client-side code.

  1. Babel Setup: Configure @babel/preset-env and @babel/preset-react to transpile JSX and modern JavaScript.
  2. Webpack Client Config: Bundles client.js and outputs it to the public/bundle.js file so the browser can download it.
  3. Webpack Server Config: Bundles server.js using target: 'node' and webpack-node-externals to ensure Node-specific modules are not bundled.

Step 4: Run the Application

Once your build tools are configured, compile your assets and start the Node server:

  1. Run Webpack to build the client and server bundles.
  2. Start the Express server by running node dist/server.js.

When a user visits your site, the server will instantly return the pre-rendered HTML containing your React components, followed by the activation of event listeners once bundle.js loads.


Alternative: Production-Ready Frameworks

While building a custom SSR setup is highly educational, maintaining Webpack configurations, code-splitting, and data-fetching synchronization between the server and client can become complex. For production environments, industry-standard frameworks automate this entire process: