Server-Side Rendering in React

By Syncfusion®, Inc. · Last updated

How SSR works: the server rendering process, request-response lifecycle, HTML generation, hydration, server-side data fetching, SSR pros and cons, and a Blog Portal capstone to download.

Introduction to Server-Side Rendering

Server-side rendering (SSR) means React runs on the server first. The server turns your components into finished HTML and sends it to the browser. The user sees the page at once, before any JavaScript has run.

Then the JavaScript loads, and React makes the page interactive. This step is called hydration.

Real-World Example

A bookshop's book page must appear in Google results and show quickly on slow phones. For each request to /books/42, the server loads the book, runs renderToString(<BookPage />), and sends finished HTML.

The user sees the title and price at once. Then the JavaScript loads, and hydrateRoot makes the Add to cart button work.

Server-side rendering: the server runs React and sends finished HTML, then the browser adds interactivity

Browser asks for /books

Server runs React

Full HTML <h1>Books...

Page visible now Then the JavaScript loads and hydrates the page

The user sees content before any JavaScript has run.

You rarely build SSR by hand. Frameworks like Next.js, React Router (Remix) and Astro do it for you. This lesson shows what happens inside, using React's real functions renderToString and hydrateRoot. In the labs, one file plays the part of the server.

Benefits of SSR

  • Fast first view. Content shows as soon as the HTML arrives, even on slow phones.
  • Better SEO. Search engines read full HTML, not an empty root div.
  • Good link previews. Social apps show the real title and text when someone shares a link.
  • Works before JavaScript. Users can read the page while the JavaScript is still loading.

Timeline: with CSR the screen is blank until JavaScript runs. With SSR content shows first and becomes interactive later CSR

blank page

load JS

content + interactive SSR

server

content visible (not interactive)

hydrated: interactive

time →

SSR shows content sooner. Both become interactive when the JavaScript is ready.

SSR Architecture

An SSR app has two parts that use the same components:

  • Server (Node.js): gets a request, renders the components to HTML, sends it.
  • Browser: loads the same components as JavaScript, and hydrates the HTML.

Because the code runs in two places, it must work in both. For example, window and document exist only in the browser, so do not use them while rendering.

SSR architecture: the same React components run on the server to make HTML and in the browser to hydrate it

Your components App.tsx

Server (Node.js) renderToString → HTML

Browser hydrateRoot → events

One set of components, two places they run.

Server Rendering Process

On the server, React uses renderToString from react-dom/server:

import { renderToString } from "react-dom/server";

const html = renderToString(<App />);
// "<div><h2>Book Corner</h2>...</div>"

It calls your components once and returns plain HTML text. It does not run useEffect and does not attach click handlers. The console in the lab shows the exact string.

Request-Response Lifecycle

When a user opens https://bookcorner.dev/posts/2:

  1. The browser sends a request for /posts/2.
  2. The server matches the route: /posts/:id with id = 2.
  3. The server fetches the data for post 2.
  4. The server renders the page to HTML with that data.
  5. The server sends the response: a status code (200, or 404) and a full HTML page.

Request-response lifecycle: request, match route, fetch data, render HTML, send response

Request GET /posts/2

Match route /posts/:id

Fetch data post 2

Render to HTML

Response 200 + HTML

all of this happens before the browser shows anything

The server does the work, then sends one finished page.

HTML Generation on the Server

The server does not send only the app's HTML. It puts it into a full page template, at a placeholder inside the root div. It also adds the page data and the <script> tag for the JavaScript.

Click the buttons in the lab to send "requests". The pretend server picks the page, sets the status, and returns a complete HTML document.

HTML generation: the server puts the app's HTML and data into a page template

<html><body> <div id="root">

<!--app-html--> </div>

<!--app-data-->

renderToString(<App />)

JSON of the page data

Two placeholders are filled, and the page is ready to send.

SSR Rendering Fundamentals

Initial Page Render

With SSR, the first page the user opens is rendered on the server. The browser receives full HTML and shows it at once. There is no blank screen.

Server-Generated Content

Compare what the browser receives:

<!-- CSR: empty until JavaScript runs -->
<div id="root"></div>

<!-- SSR: the content is already there -->
<div id="root"><h2>Book Corner</h2><ul><li>The Alchemist - ₹299</li>...</ul></div>

This server-generated content is plain HTML. Search engines and slow phones can read it with no JavaScript at all.

Client Interactivity

Server HTML looks ready, but it is not interactive yet. Buttons and links handled by React do nothing until the JavaScript loads and hydrates the page. After that, the app works like any CSR app.

Press Load page in the simulator below. Watch the stages, and try the mock page's button at each stage.

Easy Way to Remember: the server makes it visible, hydration makes it clickable.

Hydration Process

What is Hydration?

Hydration is how React makes server HTML interactive. React walks through the HTML already on the page, matches it with your components, and connects them. It does not throw the HTML away and draw it again.

In the browser, use hydrateRoot instead of createRoot:

import { hydrateRoot } from "react-dom/client";

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

Hydration: React attaches event handlers to HTML that is already on the page, so it becomes interactive

Atomic Habits ₹499

Add to cart looks ready, but clicks do nothing

hydrate

Atomic Habits ₹499

Add to cart same HTML, now clicks work

Hydration does not redraw the page. It wakes up the HTML that is already there.

Attaching Event Handlers

Server HTML has no onClick code in it: HTML text cannot hold JavaScript functions. During hydration, React finds each element and attaches the handlers from your components. It also starts state and runs useEffect.

In the lab, the server HTML is shown at once, and the "JavaScript" arrives 3 seconds later. Click the button before and after.

Hydrated React Applications

After hydration, the app is a normal React app. State, events, effects and client-side navigation all work. So SSR handles the first page, and CSR takes over after that.

One rule matters: the first browser render must make the same HTML as the server. If not, React reports a hydration mismatch and redraws that part. Values like Math.random(), Date.now() or window.innerWidth cause this. The lab shows one.

To show a browser-only value (the time, the screen size, a random tip), start with a fixed value, then change it in useEffect. Effects run only in the browser, after hydration, so the first HTML still matches.

Data Fetching in SSR

Server-Side Data Fetching

In CSR, the browser fetches data after the page loads, so users see "Loading...". In SSR, the server fetches data before rendering. The HTML already contains the data, so there is no loading state on the first view.

Passing Data to Components

The server passes the data to the components as props, then renders. The browser needs the same data to hydrate, so the server also puts it in the page:

<script>window.__INITIAL_DATA__ = {"posts":[...]}</script>

The browser reads it and hydrates with it. No second fetch.

Data in SSR: fetched on the server, passed as props, and sent along so the browser can hydrate without fetching again

Database / API posts

fetch

Server loadData()

props

<App initial=...> HTML with data

window.__INITIAL_DATA__ = { posts: [...] }

The same data is used twice: to make the HTML, and to hydrate it.

Rendering Dynamic Content

Dynamic content changes from request to request: the newest posts, today's prices, the user's name. With SSR, every request gets HTML made with fresh data. The lab fetches posts "on the server", renders them, and hydrates with the same data.

Advantages and Limitations of SSR

AdvantagesLimitations
Fast first view, no blank pageNeeds a Node.js server, not just static files
Full HTML for search engines and link previewsThe server works on every request, so it costs more
Readable before JavaScript loadsVisible before clickable: a gap until hydration
Fresh data in every responseCode must run in Node and the browser (no window in render)
Better on slow phonesHydration mismatches must be avoided

Use SSR for public pages where the first view and search matter: blogs, shops, news. CSR is usually enough for apps behind a login.

CSRSSR
First HTMLEmpty root divFull content
First viewAfter JS runsImmediately
InteractiveAfter JS runsAfter hydration
HostingStatic filesNode.js server

Capstone Project: Blog Portal

You will build a small Blog Portal for Book Corner with SSR. It has a listing page, a details page for each post, server data fetching, dynamic routes, hydration, and loading and error states. The pretend API has posts at https://api.bookcorner.dev/posts.

Blog Portal routes: the list page and a dynamic details page, both rendered on the server

/ BlogListPage

/posts/:id BlogPostPage

anything else 404 page

:id is a dynamic part: /posts/1, /posts/2, ...

One route pattern serves every post.

Create a blog listing page

BlogListPage gets the posts as a prop and shows a card for each: title (as a link), author, date and excerpt. It does not fetch anything: the data comes from the server.

Create a blog details page

BlogPostPage gets one post and shows the full text, plus a LikeButton. The button has state and a click handler, so it only works after hydration.

The lab renders both pages on the "server" with sample data, so you can see their HTML.

Fetch content on the server

routes.ts has loadData(route). The server calls it before rendering. It returns everything the page needs, called the page data: the status, plus posts, or one post, or an error.

Implement dynamic routes

matchRoute(url) turns a URL into a route. The pattern /^\/posts\/(\d+)$/ matches /posts/ followed by digits. The digits are the dynamic part, the id. So one route serves every post: /posts/1, /posts/2, and so on. An unknown id gives status 404.

In the lab, click the URLs to see the route, the status and the HTML the server makes for each.

Hydrate pages for client interaction

server.tsx puts the steps together: match the route, load the data, render <App initial={data} />. Then main.tsx shows that HTML, saves the data in window.__INITIAL_DATA__, and hydrates with the same data.

After hydration, App takes over in the browser. Clicking a link calls navigate(url). It fetches the next page's data in the browser and renders it there, with no full page reload. This is the CSR part of an SSR app.

Handle loading and error states

  • Loading: during client navigation, App shows "Loading..." until the data arrives.
  • Not found: an unknown post id gives status 404 and a friendly message.
  • Server error: a failed request gives status 500 and "Could not load this page. Please try again."

Here is the finished Blog Portal. Open a post, press Like, and try Missing post and Broken page.

Download Project Files

Download SSR Project Source Code

The complete Blog Portal is ready as a real SSR project, built with Vite, React, TypeScript, React Router and a small Express server. It works like the final lab, but the server is real: view the page source in your browser and you will see the full HTML of the posts.

Download SSR Project: Download Blog Portal Project

blog-portal/
  package.json          scripts and dependencies
  server.js             Express server: SSR for pages + JSON API
  vite.config.ts        Vite settings
  index.html            template with <!--app-html--> and <!--app-data-->
  src/
    entry-server.tsx    render(url): fetch data + renderToString
    entry-client.tsx    hydrateRoot with window.__INITIAL_DATA__
    App.tsx             layout and routes
    routes.ts           route matching and loadPageData()
    pages/              BlogListPage, BlogPostPage, NotFoundPage
    components/         Header, PostCard, LikeButton, StatusMessage
    data/               posts.ts and postsApi.ts (the "database")
    hooks/              usePageData (server data first, then client fetch)
    types/              Post and PageData types

Project Setup Instructions

  1. Install Node.js 18 or newer from nodejs.org, if you do not have it.
  2. Download the ZIP with the link above, and unzip it. You get a folder called blog-portal.
  3. Open a terminal in that folder (in VS Code: File, Open Folder, then Terminal, New Terminal).
  4. Install the packages and start the server:
npm install
npm run dev

Open http://localhost:5173. Press Ctrl + C to stop. For production, run npm run build and then npm run start.

To see SSR at work, open a post, then right-click and choose View page source. The post's text is in the HTML. In the CSR Product Catalog, the source showed only an empty root div.

Common mistakes

MistakeWhat you seeFix
Using window or localStorage during renderServer error: window is not definedUse them in useEffect, which runs only in the browser
Date.now() or Math.random() in rendered outputHydration mismatch in the consoleStart with a fixed value, change it in useEffect
Using createRoot on server HTMLThe page is drawn again and may flickerUse hydrateRoot
Hydrating with different data than the server usedMismatch errors, content jumpsSend the data in the page and hydrate with it
Fetching the same data again after hydrationAn extra request and a loading flashUse the server data for the first page
Expecting useEffect to run on the serverMissing data in the server HTMLFetch data before renderToString
Returning 200 for a missing pageSearch engines index error pagesSet status 404 when the data is not found

Check Yourself

Exercises

What to Remember

  • In SSR, the server matches the route, fetches data, and uses renderToString to send full HTML. Users see content before the JavaScript loads.
  • hydrateRoot attaches event handlers to that HTML. Hydrate with the same data the server used, and keep browser-only values in useEffect.
  • SSR gives a fast first view and good SEO, but needs a server and has a gap before the page is clickable. After hydration, the app works like CSR.

Next: Static Site Generation