When a React app with server-side rendering (SSR) loads, the server renders HTML and ships it to the browser. React then "hydrates" that HTML on the client — attaching event listeners, wiring up state, and validating that the DOM matches what React would have rendered. The moment something is different between server and client, React throws a hydration mismatch error, often with the cryptic message "Text content does not match" or "Hydration failed because the initial UI does not match what was rendered on the server." These are react hydration errors, and they're one of the trickiest problems in SSR apps.
The core issue is that server-side rendering and client-side JavaScript run in different environments. The server has no access to window, the current time, or browser-specific APIs. The client has all of them. When your component logic depends on these values, the server and client render different HTML, and React's hydration fails.
What causes React hydration errors
The most common culprits are values that differ between the server and client:
Dates and timestamps. A component that renders the current date using new Date() will produce different output on the server (when the page was rendered) and the client (when JavaScript loads, seconds later). The mismatch breaks hydration.
// ❌ This causes hydration errors
export function PostedAt() {
const now = new Date().toLocaleString();
return <span>{now}</span>;
}
Timezones and localization. The server might assume UTC while the client uses the user's local timezone. A timestamp formatted differently server-side vs. client-side causes a mismatch.
Browser APIs that don't exist on the server. Any call to window, document, localStorage, or navigator returns undefined on the server. Code that branches based on these values will render one way server-side and differently client-side.
// ❌ Hydration error: window.matchMedia is undefined on the server
export function ResponsiveComponent() {
const isMobile = typeof window !== "undefined" &&
window.matchMedia("(max-width: 768px)").matches;
return isMobile ? <Mobile /> : <Desktop />;
}
Random values and unique IDs. Calling Math.random() or crypto.getRandomValues() server-side will produce different output than client-side, even for the same component.
Dynamic content like user preferences, A/B test assignments, or feature flags that are fetched after the initial page load. The server renders with one value, the client loads a different value, and React fails to reconcile.
How hydration errors show up in production
When a hydration mismatch occurs, React logs a warning in the browser console. In development, you'll see a detailed error and a highlighted DOM node. In production, React becomes stricter — it may re-render the component from scratch, discarding the server-rendered HTML and replacing it client-side. This isn't catastrophic, but it defeats the purpose of SSR (faster first paint) and can cause a jarring flicker or flash of unstyled content (FOUC).
Worse, the console warning is easy to miss if you're not actively monitoring browser errors. That's why proper javascript error monitoring is essential for SSR apps — hydration warnings should be captured and grouped so you catch regressions before they affect users.
Hydration errors don't usually crash the app entirely. Instead, React recovers by re-rendering on the client. But the performance benefit of SSR is lost, and the user sees a flicker. In production, this often goes unnoticed unless you're tracking console warnings and errors.
Fixing hydration errors: strategies and patterns
Defer browser-only code to useEffect. The safest pattern is to render the same markup server-side and client-side, then use useEffect to patch in browser-specific content:
export function PostedAt({ date }) {
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return <span>{date}</span>; // Static date from server
}
// Render client-only content after mount
return <span>{new Date().toLocaleString()}</span>;
}
This renders the same HTML server-side and initially on the client, so hydration passes. Then useEffect runs, sets mounted to true, and re-renders with dynamic content.
Use suppressHydrationWarning. For cases where a mismatch is unavoidable or harmless, React provides an escape hatch:
export function ClientOnlyWidget() {
return <div suppressHydrationWarning>{Math.random()}</div>;
}
Use this sparingly — it silences the warning but doesn't solve the underlying mismatch. Only use it when you're certain the difference won't cause layout shift or other user-facing issues.
Ensure stable, deterministic rendering. Avoid random values, dates, and time-dependent logic in the initial render. Pass stable data from the server so both server and client render the same HTML:
// ✅ Server and client both render the same timestamp
export function PostedAt({ timestamp }) {
return <span>{new Date(timestamp).toLocaleString()}</span>;
}
Use a key that doesn't change between renders. Hydration errors can also occur with dynamic lists if the key changes:
// ❌ Math.random() creates a new key on every render
{items.map((item) => (
<div key={Math.random()}>{item.name}</div>
))}
// ✅ Use a stable identifier
{items.map((item) => (
<div key={item.id}>{item.name}</div>
))}
Preventing hydration errors early
The best defense is building with hydration in mind from the start:
-
Use a consistent source of truth. Pass data from the server to the client explicitly (via props, data attributes, or a hydration script). Don't re-fetch or re-generate it on the client.
-
Audit browser API usage. Search your codebase for
window,document,localStorage, and other browser globals. Wrap them intypeof window !== "undefined"checks or move them intouseEffect. -
Test SSR locally. When you disable JavaScript in DevTools or run your Next.js app in
output: "export"mode, you're testing what the server actually renders. Do this before shipping. -
Validate timestamps and locales. If your app depends on user timezone or language, pass it from server to client explicitly. Don't infer it from the client.
Detecting and debugging hydration errors
When hydration errors slip into production, your error tracking setup should capture them. React's hydration warnings appear as console warnings, not thrown exceptions, so you need to explicitly monitor them:
React.useEffect(() => {
const originalWarn = console.warn;
console.warn = (...args) => {
if (
typeof args[0] === "string" &&
args[0].includes("Hydration failed")
) {
Sentry.captureMessage("Hydration mismatch detected", "warning");
}
originalWarn(...args);
};
}, []);
With hydration errors captured, you can see which components are affected, how often it happens, and whether it correlates with a recent deploy. Check the breadcrumbs to see what the user was doing, and inspect the server and client markup to find the divergence.
Document the mismatch in your error report. Log the rendered HTML, the browser capabilities that differ, and the release version. This context speeds up diagnosis — you can often spot the problematic code without debugging.
Learn more
For a broader look at React error tracking, see error tracking in React. If you're using Next.js or another SSR framework, Next.js error tracking covers framework-specific patterns. And for strategies to catch all browser errors — not just hydration — global JavaScript error handling covers the full picture.
Start tracking errors in minutes
Catch React hydration errors in production and see exactly which users hit them — point the Sentry SDK at LightTrace and start shipping SSR apps with confidence.
Hydration errors are subtle but solvable. The pattern is simple: render the same thing server-side and client-side, defer browser-only logic to useEffect, and monitor for warnings in production. With those habits in place, your SSR app stays fast and stable.