Fix React hydration error: text mismatch
React throws this when client and server HTML differ after hydration. Most common cause: date or time rendering that doesn't match timezone.
1. Date or time rendering mismatch (the most common cause)
Had a client last month whose Next.js ecommerce site started throwing this error on every product page. The culprit? A Date.now() call inside a component that showed 'Last updated 2 minutes ago'. The server rendered with UTC time, the client with local time. Boom — hydration error.
The fix is dead simple: don't render anything that depends on the current time, timezone, or locale during server-side rendering. Use useEffect to set the value after mount, or use Next.js dynamic imports with ssr: false.
import { useEffect, useState } from 'react';
function TimeAgo({ timestamp }) {
const [label, setLabel] = useState(null);
useEffect(() => {
setLabel(timeSince(timestamp)); // runs only on client
}, [timestamp]);
if (!label) return null; // or a placeholder
return {label};
}
If you're using React 18's streaming SSR, you can also wrap the component in a <Suspense> boundary. But honestly, the useEffect approach is cleaner for most cases.
2. localStorage or browser API references during SSR
Another classic: checking typeof window !== 'undefined' to access localStorage or sessionStorage in a conditional render. You'd think it's safe, but React's hydration still sees the mismatch between the server-rendered branch and the client branch.
I saw this on a Next.js dashboard where a dark mode toggle checked localStorage for a saved preference. The server rendered the toggle off, the client read localStorage and toggled it on — hydration error.
Solution: delay the localStorage read until after hydration. Same pattern with useEffect:
function ThemeToggle() {
const [theme, setTheme] = useState(null);
useEffect(() => {
const saved = localStorage.getItem('theme') || 'light';
setTheme(saved);
}, []);
// render nothing until we know the theme
if (!theme) return null;
return ;
}
If you can't avoid rendering something initially, use suppressHydrationWarning on the wrapping element. But be warned: this tells React to ignore the mismatch, which can lead to flickering. Use it sparingly.
3. Third-party scripts modifying the DOM before hydration
This one is sneaky. A third-party script (analytics, chat widget, A/B testing tool) runs before React hydration finishes and changes the HTML. React then tries to hydrate the altered DOM and fails.
I debugged this for a SaaS client whose Intercom widget was injecting a <style> tag into the <head> before React could hydrate. The server-rendered <head> didn't have that style, so hydration barfed.
Fix: defer third-party scripts until after React mounts. Add defer or async to script tags, or load them programmatically in a useEffect:
useEffect(() => {
const script = document.createElement('script');
script.src = 'https://third-party.com/widget.js';
script.async = true;
document.body.appendChild(script);
}, []);
If you're using Next.js, wrap the script in next/script with strategy="afterInteractive". That delays execution until after hydration.
Quick-reference summary table
| Cause | Symptoms | Fix |
|---|---|---|
| Date/time rendering | Text like '2 minutes ago' or formatted dates differ | Use useEffect to set value after mount |
| localStorage/ browser APIs | UI depends on client-only data (theme, user prefs) | Defer reads until hydration completes |
| Third-party scripts | Scripts inject DOM elements before React hydrates | Load scripts after hydration (defer/useEffect) |
One last thing: if you can't figure out which element is causing the error, open your browser console. React logs the mismatched HTML in the error message — look for the Server: ... and Client: ... lines. That'll point you straight to the culprit.
Was this solution helpful?