Server-Side Rendering (SSR) in React: A Comprehensive Guide

Rahul JainRahul Jain
6 min read

React.js, since its inception, has taken the front-end development world by storm due to its component-based architecture, virtual DOM, and efficient rendering techniques. React applications, by default, use a rendering strategy known as Client-Side Rendering (CSR). In CSR, all the JavaScript is loaded on the client side, and the user’s browser takes responsibility for rendering the application after the JavaScript bundle is downloaded and executed. However, for performance, SEO, and user experience reasons, developers often opt for Server-Side Rendering (SSR) in React.

SSR enables rendering a React application on the server before sending the fully rendered page to the client. This approach has many benefits, including faster initial page load and improved search engine optimization (SEO). In this article, we’ll dive deep into how SSR works, its benefits, and how to implement it with React.


What is Server-Side Rendering (SSR)?

Server-Side Rendering (SSR) is the process of rendering React components on the server and sending a fully rendered HTML page to the browser. When a user requests a page, the server executes the React components, generates the HTML markup, and returns it to the client. Once the HTML is loaded by the browser, React takes over the application and hydrates it to become fully interactive.

This contrasts with Client-Side Rendering (CSR), where the server sends a bare HTML file with a JavaScript bundle, and the browser has to render the application entirely on the client side.


Benefits of SSR in React

  1. Improved SEO: Since web crawlers often have trouble executing JavaScript (despite improvements), SSR helps by sending fully rendered HTML to these bots, ensuring they can index the content. This is particularly beneficial for content-heavy websites such as blogs or e-commerce platforms.

  2. Faster Initial Load: SSR delivers a pre-rendered HTML page, ensuring that users see the content faster, even before the JavaScript has been fully downloaded and executed. This improves the perception of load time.

  3. Better Performance for Slow Networks: Since the HTML is rendered on the server, users with slower devices or networks don’t have to wait for a heavy JavaScript bundle to be downloaded and executed before they can view the content.

  4. Improved Time to First Byte (TTFB): SSR can reduce TTFB, as the server provides the initial HTML response quickly. This helps improve page load times and user experience.

  5. Content Availability for Bots: With SSR, search engine bots, scrapers, and crawlers can directly access the content from the HTML instead of trying to execute JavaScript.


How Does SSR Work in React?

In a typical React SSR flow:

  1. The user makes a request for a web page.

  2. The server loads the necessary React components and fetches any required data (e.g., via an API).

  3. The server renders the React components to an HTML string using ReactDOMServer.renderToString() or ReactDOMServer.renderToStaticMarkup().

  4. The server sends the fully rendered HTML as a response.

  5. The client loads the JavaScript bundle, and React hydrates the app, making it interactive.

React’s hydration process ensures that the static HTML sent by the server becomes fully interactive once the client-side React app is loaded.


Implementing SSR in React

Let’s walk through the steps of setting up SSR in a React application.

1. Setting Up a Basic React Application

Start by creating a basic React application if you don’t already have one. You can do this with create-react-app or set it up manually.

npx create-react-app react-ssr-example
cd react-ssr-example

Install necessary dependencies:

npm install express react-dom react-router-dom

The above dependencies include:

  • Express: A Node.js web framework that we'll use to handle HTTP requests.

  • React Router DOM: Used for routing in React apps.

  • React DOM: Provides methods to interact with the DOM, including renderToString for SSR.

2. Building the Server

In SSR, the server must handle rendering the React application. Create a server.js file in the root of your project:

// server.js
import path from 'path';
import fs from 'fs';
import express from 'express';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import App from './src/App';

const PORT = 8000;
const app = express();

app.use(express.static(path.resolve(__dirname, 'build')));

app.get('/*', (req, res) => {
  const app = ReactDOMServer.renderToString(<App />);

  const indexFile = path.resolve('./build/index.html');
  fs.readFile(indexFile, 'utf8', (err, data) => {
    if (err) {
      console.error('Error reading index.html', err);
      return res.status(500).send('Oops, better luck next time!');
    }

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

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

Explanation:

  1. Express Setup: We use Express to create a basic HTTP server that listens for incoming requests.

  2. Rendering the App: For every request, we call ReactDOMServer.renderToString(<App />), which converts the entire React component tree into an HTML string.

  3. Serving the HTML: We replace the root div inside index.html with the rendered HTML from React. This ensures that the HTML we send to the client is fully pre-rendered.

3. Modify the React Application

For this example, modify App.js to include some sample content and routing:

// src/App.js
import React from 'react';
import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom';

const Home = () => <h1>Home Page</h1>;
const About = () => <h1>About Page</h1>;

const App = () => {
  return (
    <Router>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
      </nav>
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/about" component={About} />
      </Switch>
    </Router>
  );
};

export default App;

Here, we have simple navigation between a home page and an about page.

4. Build and Run the Application

To serve the React app, build the project using create-react-app's build tool:

npm run build

Once the build process completes, run the server:

node server.js

Open your browser and navigate to http://localhost:8000. You should see your React app being served with SSR!

5. Hydration on the Client

Once the server sends the pre-rendered HTML, React needs to hydrate it on the client side. Ensure your index.js looks like this:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

ReactDOM.hydrate(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

The ReactDOM.hydrate() method is used to attach React to the pre-rendered HTML, making it interactive. Unlike ReactDOM.render(), it preserves the existing HTML and adds interactivity, which is essential for SSR.


Handling Data Fetching in SSR

One challenge with SSR is handling asynchronous data fetching. To perform SSR with data, you need to wait for the data to be fetched before rendering the components on the server.

A typical solution is to have some logic in your server-side code to fetch the data, then pass it as props to the React components. Tools like Redux or React’s Context API can be helpful in managing server-side data.

For instance, using Redux, you can:

  1. Fetch data on the server.

  2. Populate the Redux store with the data.

  3. Render the React components with the Redux provider and the store.

  4. Send the pre-rendered HTML along with the initial state of the Redux store to the client.


Code Splitting and SSR

React provides the ability to split your application into smaller chunks using React.lazy and Suspense for client-side rendering. However, these don’t work directly with SSR. For SSR, you can use a library like loadable-components to enable code splitting with SSR.


Limitations of SSR

While SSR offers significant advantages, it also comes with challenges:

  1. Increased Server Load: Since rendering is done on the server, SSR can increase the load on the server, especially with complex UIs.

  2. Complexity: Implementing SSR can make the app architecture more complex, especially with asynchronous data fetching and routing.

  3. Slower TTFB for Dynamic Pages: For highly dynamic applications, SSR may introduce delays because the server has to fetch data, render the HTML, and then send it to the client.


0
Subscribe to my newsletter

Read articles from Rahul Jain directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Rahul Jain
Rahul Jain