Difficulty: Advanced
What is server-side rendering (SSR) in React? What is hydration and what causes hydration mismatches?
Server-Side Rendering, Static Site Generation, and Hydration are topics that come up constantly in modern React development, especially with frameworks like Next.js being so popular. Let me break these down clearly because they are related but distinct concepts, and understanding the nuances matters.
Let us start with the problem these solve. In a traditional React single-page application (SPA), the server sends a mostly empty HTML file with a script tag. The browser downloads the JavaScript bundle, executes it, React builds the component tree, and finally renders the UI. Until all of that happens, the user stares at a blank white screen. This is bad for two reasons: first, the user experience is poor because there is a noticeable delay before anything appears. Second, search engine crawlers may not execute JavaScript, so they see an empty page, which hurts SEO.
Server-Side Rendering (SSR) flips the process. Instead of sending empty HTML, the server runs your React components, generates the full HTML, and sends that to the browser. The user immediately sees the content -- text, images, layout -- even before any JavaScript loads. Then, once the JavaScript bundle downloads and executes, React takes over and makes the page interactive. The user gets fast visual feedback AND a fully interactive app.
Static Site Generation (SSG) is similar but happens at build time instead of request time. When you deploy your app, the build process renders each page to HTML and saves those files. When a user requests a page, the server just serves the pre-built HTML file -- no rendering computation needed. This is incredibly fast because serving a static file is the fastest thing a server can do. SSG is perfect for content that does not change often: blog posts, documentation, marketing pages, product listings.
Next.js also offers Incremental Static Regeneration (ISR), which is a hybrid: pages are statically generated at build time but can be revalidated in the background after a specified interval. So your product page might be rebuilt every 60 seconds. Users get the speed of static serving with reasonably fresh data.
Now, here is where it gets interesting: hydration. After the server sends the pre-rendered HTML and the browser displays it, the page looks right but is not interactive. Buttons do not respond to clicks, forms do not submit, state does not update. The HTML is just static content at this point. Hydration is the process where React attaches to this existing HTML, hooks up all the event listeners, initializes state, and makes everything interactive. It is like bringing a mannequin to life -- the body (HTML) is there, but hydration adds the brain (JavaScript interactivity).
During hydration, React walks through the server-rendered HTML and the component tree it would render on the client, comparing them. If they match, React just attaches event listeners without re-rendering the DOM. If they do NOT match, you get a hydration mismatch, which is one of the most common and frustrating bugs in SSR applications.
Hydration mismatches happen when the server-rendered HTML is different from what React would render on the client. Common causes include: using Date.now() or new Date() (the server and client render at different times, producing different timestamps), Math.random() (different random values on server and client), checking window.innerWidth or navigator.userAgent (window does not exist on the server), conditional rendering based on whether the user is logged in (the server might not have the auth token available in the same way), and using browser-only APIs like localStorage.
When a hydration mismatch occurs, React warns in the console and may throw away the server-rendered HTML entirely, re-rendering from scratch on the client. This defeats the entire purpose of SSR because the user sees the server-rendered content, then it disappears and re-renders. Sometimes the content even flashes or jumps.
The fixes for hydration mismatches depend on the cause. For values that are intentionally different between server and client (like timestamps), you can use the suppressHydrationWarning prop on the specific element. For browser-only code, use useEffect to run it only on the client (since useEffect never runs on the server). For random IDs, React 18 introduced the useId hook, which generates stable IDs that match between server and client. For components that simply cannot work on the server (like maps that depend on window), use dynamic import with ssr: false in Next.js.
React 18 brought major improvements to SSR with Streaming SSR and Selective Hydration. Instead of waiting for the entire page to render on the server before sending anything, streaming SSR sends HTML in chunks as components finish rendering. Combined with Suspense boundaries, parts of the page that depend on slow data can stream in later while the rest of the page is already visible and hydrating.
React Server Components (RSC), introduced with React 18 and used extensively in Next.js App Router, take this even further. Server Components render only on the server and send serialized output (not HTML, but a special format) to the client. They never hydrate because they have no client-side JavaScript. This means you can import heavy libraries, query databases, or access the filesystem directly in your component code without any of that adding to the client bundle. Only components that need interactivity (event handlers, state, effects) are marked as Client Components with the 'use client' directive, and only those get hydrated.
Understanding these concepts is essential for modern React development. The trend is clearly toward server-first rendering with minimal client-side JavaScript. Whether you use Next.js, Remix, or another framework, the underlying principles of SSR, SSG, hydration, and now RSC are the same.
// pages/index.tsx (Next.js Pages Router)
export async function getServerSideProps() {
const posts = await fetchPosts(); // Runs on server
return { props: { posts } };
}
// SSG - rendered at build time
export async function getStaticProps() {
const posts = await fetchPosts();
return {
props: { posts },
revalidate: 60 // ISR: rebuild every 60 seconds
};
}
export default function Home({ posts }) {
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
// Next.js App Router - React Server Components
// app/page.tsx
async function HomePage() {
const posts = await fetchPosts(); // Runs on server only
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
getServerSideProps runs per request; getStaticProps runs at build. RSC in App Router always runs on the server with no client bundle.
// PROBLEM: window is not available on server
function Component() {
const width = window.innerWidth; // ReferenceError on server!
return <div>Width: {width}</div>;
}
// FIX 1: Check environment
function ComponentFixed() {
const [width, setWidth] = useState(0); // 0 on server
useEffect(() => {
setWidth(window.innerWidth); // Runs only on client
}, []);
return <div>Width: {width}</div>;
}
// FIX 2: suppressHydrationWarning for known mismatches
function DateTime() {
return (
<time suppressHydrationWarning>
{new Date().toLocaleString()} {/* Different on server vs client */}
</time>
);
}
// FIX 3: Client-only rendering
import dynamic from 'next/dynamic';
const MapComponent = dynamic(
() => import('./Map'),
{ ssr: false } // Skip SSR entirely for this component
);
// PROBLEM: Random IDs
const id = Math.random(); // Different each render - mismatch!
// FIX: React 18's useId hook
import { useId } from 'react';
const stableId = useId(); // Stable across server and client
Hydration mismatches cause React to re-render from scratch on the client (slow). suppressHydrationWarning, useEffect-gated access, and useId solve the most common causes.
SSR, SSG, Hydration, Hydration Mismatch, Next.js