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
rootdiv. - 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:
- The browser sends a request for
/posts/2. - The server matches the route:
/posts/:idwithid = 2. - The server fetches the data for post 2.
- The server renders the page to HTML with that data.
- 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
| Advantages | Limitations |
|---|---|
| Fast first view, no blank page | Needs a Node.js server, not just static files |
| Full HTML for search engines and link previews | The server works on every request, so it costs more |
| Readable before JavaScript loads | Visible before clickable: a gap until hydration |
| Fresh data in every response | Code must run in Node and the browser (no window in render) |
| Better on slow phones | Hydration 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.
| CSR | SSR | |
|---|---|---|
| First HTML | Empty root div | Full content |
| First view | After JS runs | Immediately |
| Interactive | After JS runs | After hydration |
| Hosting | Static files | Node.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,
Appshows "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
- Install Node.js 18 or newer from nodejs.org, if you do not have it.
- Download the ZIP with the link above, and unzip it. You get a folder called
blog-portal. - Open a terminal in that folder (in VS Code: File, Open Folder, then Terminal, New Terminal).
- 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
| Mistake | What you see | Fix |
|---|---|---|
Using window or localStorage during render | Server error: window is not defined | Use them in useEffect, which runs only in the browser |
Date.now() or Math.random() in rendered output | Hydration mismatch in the console | Start with a fixed value, change it in useEffect |
Using createRoot on server HTML | The page is drawn again and may flicker | Use hydrateRoot |
| Hydrating with different data than the server used | Mismatch errors, content jumps | Send the data in the page and hydrate with it |
| Fetching the same data again after hydration | An extra request and a loading flash | Use the server data for the first page |
Expecting useEffect to run on the server | Missing data in the server HTML | Fetch data before renderToString |
| Returning 200 for a missing page | Search engines index error pages | Set 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
renderToStringto send full HTML. Users see content before the JavaScript loads. hydrateRootattaches event handlers to that HTML. Hydrate with the same data the server used, and keep browser-only values inuseEffect.- 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