
React Errors Hydration
- 11 installs
- 6 repo stars
- Updated July 8, 2026
- openaec-foundation/react-claude-skill-package
Helps with frontend development tasks.
About
react-errors-hydration is a Claude Code skill for frontend development. It helps solo builders move faster with AI-assisted development.
- react-errors-hydration
- Frontend Development
- AI-coding skill
React Errors Hydration by the numbers
- 11 all-time installs (skills.sh)
- Ranked #1,658 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/react-claude-skill-package --skill react-errors-hydrationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 11 |
|---|---|
| repo stars | ★ 6 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/react-claude-skill-package ↗ |
What it does
Helps with frontend development tasks.
Files
react-errors-hydration
Quick Reference
What Hydration Is
Hydration is the process where React attaches event handlers and component state to server-rendered HTML. React expects the server-rendered DOM to EXACTLY match what the client would render on its first pass. Any difference triggers a hydration mismatch error.
Server renders HTML string ──> Browser displays HTML (fast, non-interactive)
│
React hydrates ────────────────> Attaches event handlers, state, effects
│
Interactive applicationALWAYS ensure server and client render identical output on the first render pass. Hydration does NOT patch differences -- it assumes the DOM is correct and only attaches interactivity.
NEVER treat hydration warnings as harmless. In React 18, mismatches silently produce broken UI. In React 19, React attempts recovery but at a performance cost.
---
Hydration Mismatch Diagnostic Table
| Error Message / Symptom | Cause | Fix |
|---|---|---|
| "Text content does not match" | Different text on server vs client (Date, Math.random, locale) | Use useEffect + state for dynamic values |
"Expected server HTML to contain a matching <div> in <p>" | Invalid HTML nesting (<div> inside <p>, <p> inside <p>) | Fix HTML nesting to follow spec |
| "Hydration failed because the server-rendered HTML didn't match the client" | Conditional rendering based on client-only state | Use useEffect for client-only branches |
| "There was an error while hydrating but React was able to recover" (React 19) | Any mismatch -- React 19 reports and recovers | Fix root cause; recovery re-renders entire tree |
| Content flickers on page load | Mismatch causes React to discard server HTML and re-render | Identify and fix the mismatch source |
| Event handlers not working on server-rendered content | Hydration failed silently (React 18) | Check console for hydration warnings |
Extra attributes like data-* from browser extensions | Extensions inject attributes after server render | Ignore if confirmed extension-caused; see Browser Extensions section |
"Prop className did not match" | CSS-in-JS generating different class names server vs client | Configure SSR for your CSS-in-JS library |
---
Common Causes (Ranked by Frequency)
1. Date/Time Rendering
Problem: Server and client run at different times or timezones.
// BAD: Hydration mismatch -- server time !== client time
function Header(): JSX.Element {
return <span>{new Date().toLocaleTimeString()}</span>;
}
// GOOD: Render on client only via useEffect
function Header(): JSX.Element {
const [time, setTime] = useState<string>("");
useEffect(() => {
setTime(new Date().toLocaleTimeString());
}, []);
return <span>{time}</span>;
}2. Browser-Only APIs (window, localStorage, navigator)
Problem: These APIs do not exist on the server.
// BAD: window is undefined on server
function Layout(): JSX.Element {
const width = window.innerWidth;
return <div>{width > 768 ? <Desktop /> : <Mobile />}</div>;
}
// GOOD: Detect client with useEffect
function Layout(): JSX.Element {
const [isClient, setIsClient] = useState<boolean>(false);
useEffect(() => {
setIsClient(true);
}, []);
if (!isClient) {
return <div><Desktop /></div>; // Server default
}
return (
<div>{window.innerWidth > 768 ? <Desktop /> : <Mobile />}</div>
);
}3. Conditional Rendering Based on Client State
Problem: Authentication status, feature flags, or user preferences differ between server and client.
// BAD: isLoggedIn differs on server vs client
function Nav(): JSX.Element {
const isLoggedIn = checkAuth(); // Returns false on server, true on client
return isLoggedIn ? <UserMenu /> : <LoginButton />;
}
// GOOD: Start with server value, update on client
function Nav(): JSX.Element {
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
useEffect(() => {
setIsLoggedIn(checkAuth());
}, []);
return isLoggedIn ? <UserMenu /> : <LoginButton />;
}4. Math.random and Non-Deterministic Values
// BAD: Different random value on server vs client
function Banner(): JSX.Element {
const id = Math.random().toString(36).slice(2);
return <div id={id}>Welcome</div>;
}
// GOOD: Use useId (React 18+) for stable identifiers
function Banner(): JSX.Element {
const id = useId();
return <div id={id}>Welcome</div>;
}5. HTML Nesting Violations
ALWAYS follow HTML nesting rules. The browser auto-corrects invalid nesting BEFORE React hydrates, creating a DOM that does not match React's expected tree.
| Invalid Nesting | Browser Correction | Result |
|---|---|---|
<p><div>text</div></p> | Splits into <p></p><div>text</div><p></p> | Mismatch |
<a><a>link</a></a> | Closes outer <a> before inner | Mismatch |
<table><div>row</div></table> | Removes <div> | Mismatch |
<ul><div><li>item</li></div></ul> | Restructures children | Mismatch |
6. CSS-in-JS Class Name Mismatch
CSS-in-JS libraries (styled-components, Emotion) generate class names at runtime. If the server and client use different generation strategies or ordering, class names differ.
ALWAYS configure your CSS-in-JS library for SSR following its official documentation. For styled-components, use ServerStyleSheet. For Emotion, use extractCriticalToChunks.
---
The isClient Pattern
The standard pattern for client-only rendering:
function useIsClient(): boolean {
const [isClient, setIsClient] = useState<boolean>(false);
useEffect(() => {
setIsClient(true);
}, []);
return isClient;
}
// Usage
function ClientOnlyFeature(): JSX.Element {
const isClient = useIsClient();
if (!isClient) {
return <Placeholder />; // MUST match server output
}
return <RichInteractiveWidget />;
}NEVER use typeof window !== "undefined" as a render condition -- this evaluates to true during client-side rendering of SSR apps (hydration pass), causing the mismatch you are trying to avoid.
---
suppressHydrationWarning
// Acceptable: Timestamps that intentionally differ
<time suppressHydrationWarning>
{new Date().toISOString()}
</time>
// Acceptable: Third-party widget containers
<div suppressHydrationWarning id="third-party-widget" />ALWAYS use suppressHydrationWarning ONLY on individual elements where the mismatch is intentional and harmless.
NEVER use suppressHydrationWarning as a blanket fix on parent containers. It only suppresses one level deep and masks real bugs.
NEVER use suppressHydrationWarning to hide mismatches you do not understand. Diagnose the root cause first.
---
hydrateRoot API
import { hydrateRoot } from "react-dom/client";
import App from "./App";
const root = hydrateRoot(
document.getElementById("root") as HTMLElement,
<App />,
{
onRecoverableError(error: unknown, errorInfo: { componentStack?: string }) {
// Log hydration mismatches to your error tracking service
console.error("Hydration error:", error);
console.error("Component stack:", errorInfo.componentStack);
},
}
);ALWAYS provide onRecoverableError in production to track hydration issues. React 19 uses this callback for all recovered hydration mismatches.
---
React 19 Hydration Improvements
| Feature | React 18 | React 19 |
|---|---|---|
| Error messages | Generic "did not match" text | Full HTML diff showing server vs client output |
| Recovery | Discards entire server-rendered tree on mismatch | Attempts granular recovery, re-renders only affected subtree |
| Reporting | Console warning only | onRecoverableError callback with component stack |
| Third-party script interference | Silent failures | Better tolerance for extra attributes from extensions |
<style> and <link> in <head> | Manual hoisting required | Native support; React deduplicates and hoists automatically |
React 19 Diff Output Example
Warning: Text content did not match.
Server: "Hello, World"
Client: "Hello, User"
at Greeting (app/components/Greeting.tsx:5:3)
at Layout (app/layout.tsx:12:5)React 19 shows the EXACT server value vs client value plus the full component stack. Use this to trace the mismatch source directly.
---
Browser Extension Interference
Browser extensions (ad blockers, password managers, translation tools, accessibility plugins) inject or modify DOM elements AFTER server render but BEFORE React hydration.
Symptoms
- Hydration errors in production that are NOT reproducible locally
- Extra
<div>,<style>, ordata-*attributes in the DOM - Errors disappear in incognito mode
Diagnosis
1. Open the page in incognito mode (extensions disabled) 2. If the error disappears, an extension is the cause 3. Use onRecoverableError to log and filter these in production
Mitigation
- NEVER restructure your app to work around extension interference
- ALWAYS use
onRecoverableErrorto detect and filter extension-caused errors - Consider wrapping known injection targets with
suppressHydrationWarningONLY if confirmed extension-caused
---
Decision Tree: Fixing Hydration Errors
Hydration error detected
├── Is the content time-dependent (Date, timestamp)?
│ └── YES → Use useEffect + state OR suppressHydrationWarning
├── Does it use browser APIs (window, localStorage, navigator)?
│ └── YES → Use the isClient pattern or dynamic import with ssr: false
├── Is it conditional on user/auth state?
│ └── YES → Default to logged-out on server, update in useEffect
├── Is it an HTML nesting violation?
│ └── YES → Fix the HTML structure (no <div> in <p>, etc.)
├── Is it CSS-in-JS class names?
│ └── YES → Configure SSR for your CSS-in-JS library
├── Does it only happen in production with real users?
│ └── YES → Likely browser extension interference; use onRecoverableError
└── None of the above?
└── Check for: different environment variables, different API responses,
third-party scripts loading before hydration---
Dynamic Import for Client-Only Components
When a component fundamentally cannot render on the server:
// Next.js
import dynamic from "next/dynamic";
const MapWidget = dynamic(() => import("./MapWidget"), {
ssr: false,
loading: () => <div>Loading map...</div>,
});
// Generic React with React.lazy (client-only, not for SSR)
const MapWidget = React.lazy(() => import("./MapWidget"));ALWAYS provide a loading fallback that matches the server-rendered placeholder to prevent layout shift.
---
Reference Links
- references/examples.md -- Hydration error examples with complete fix patterns
- references/anti-patterns.md -- Common hydration mistakes and why they fail
Official Sources
- https://react.dev/reference/react-dom/client/hydrateRoot
- https://react.dev/link/hydration-mismatch
- https://react.dev/reference/react/useId
- https://react.dev/reference/react-dom/client/hydrateRoot#handling-different-client-and-server-content
Hydration Anti-Patterns
Anti-Pattern 1: typeof window Check in Render
What People Do
// BAD: This does NOT prevent hydration mismatches
function Feature(): JSX.Element {
if (typeof window !== "undefined") {
return <ClientFeature />;
}
return <ServerFallback />;
}Why It Fails
During hydration, typeof window !== "undefined" evaluates to true on the client. The server rendered <ServerFallback />, but the client's first render produces <ClientFeature />. This IS the mismatch.
Correct Approach
function Feature(): JSX.Element {
const [isClient, setIsClient] = useState<boolean>(false);
useEffect(() => {
setIsClient(true);
}, []);
if (!isClient) {
return <ServerFallback />;
}
return <ClientFeature />;
}useState(false) returns false on both server and client first render. useEffect runs AFTER hydration, so the switch to <ClientFeature /> happens as a normal state update.
---
Anti-Pattern 2: Blanket suppressHydrationWarning
What People Do
// BAD: Wrapping large sections to silence warnings
function App(): JSX.Element {
return (
<div suppressHydrationWarning>
<Header />
<Main />
<Footer />
</div>
);
}Why It Fails
suppressHydrationWarningonly works ONE level deep -- it suppresses warnings on the element's own attributes and text content, NOT on its children- It masks real bugs that cause broken event handlers and stale content
- The underlying mismatches still occur; you just do not see the warnings
Correct Approach
Diagnose and fix each mismatch individually. NEVER suppress warnings you do not understand.
---
Anti-Pattern 3: Direct DOM Manipulation Before Hydration
What People Do
// BAD: Script in <head> modifies DOM before React hydrates
<head>
<script dangerouslySetInnerHTML={{ __html: `
document.body.classList.add(
localStorage.getItem('theme') || 'light'
);
` }} />
</head>Why It Fails
The script adds a class to <body> that was not present in the server-rendered HTML. When React hydrates, the DOM has been modified and no longer matches.
Correct Approach
// Use a cookie or server-side session to determine theme BEFORE rendering
// OR use the isClient pattern to apply the theme after hydration
function ThemeWrapper({ children }: { children: React.ReactNode }): JSX.Element {
const [theme, setTheme] = useState<string>("light");
useEffect(() => {
const saved = localStorage.getItem("theme") || "light";
setTheme(saved);
document.body.classList.add(saved);
return () => document.body.classList.remove(saved);
}, []);
return <div data-theme={theme}>{children}</div>;
}If you MUST set the theme before paint to avoid flash of wrong theme, use a cookie that the server can read to render the correct theme on the server side.
---
Anti-Pattern 4: Using Date Directly in JSX
What People Do
// BAD: Every render produces a different string
function Footer(): JSX.Element {
return <p>Copyright {new Date().getFullYear()}</p>;
}Why It Might Fail
If the server renders at 23:59:59 on December 31st and the client hydrates at 00:00:01 on January 1st, the year differs. This is a rare but real edge case.
Correct Approach
// GOOD: Pass the year as a prop from the server, or use suppressHydrationWarning
function Footer({ year }: { year: number }): JSX.Element {
return <p>Copyright {year}</p>;
}
// OR for non-critical display:
function Footer(): JSX.Element {
return <p suppressHydrationWarning>Copyright {new Date().getFullYear()}</p>;
}---
Anti-Pattern 5: Rendering Different Content Based on User Agent
What People Do
// BAD: navigator is undefined on server
function DownloadButton(): JSX.Element {
const isMac = navigator.userAgent.includes("Mac");
return (
<a href={isMac ? "/download/mac" : "/download/windows"}>
Download for {isMac ? "macOS" : "Windows"}
</a>
);
}Why It Fails
navigator does not exist on the server. Even if you guard with typeof navigator !== "undefined", the server renders the fallback while the client renders the detected platform.
Correct Approach
function DownloadButton(): JSX.Element {
const [platform, setPlatform] = useState<"mac" | "windows" | null>(null);
useEffect(() => {
setPlatform(navigator.userAgent.includes("Mac") ? "mac" : "windows");
}, []);
if (!platform) {
return <a href="/download">Download</a>; // Generic fallback
}
return (
<a href={`/download/${platform}`}>
Download for {platform === "mac" ? "macOS" : "Windows"}
</a>
);
}---
Anti-Pattern 6: Third-Party Scripts Modifying the DOM
What People Do
// BAD: External script modifies the DOM between server render and hydration
<body>
<div id="root">{serverRenderedContent}</div>
<script src="https://third-party.com/widget.js"></script>
<script src="/bundle.js"></script> {/* React hydrates here */}
</body>Why It Fails
The third-party script may inject elements inside #root or modify existing DOM nodes before React hydrates.
Correct Approach
- Load third-party scripts AFTER hydration using
useEffect - Place third-party widget containers OUTSIDE the React root
- Use
suppressHydrationWarningon specific elements that third-party scripts modify
function ThirdPartyWidget(): JSX.Element {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// Load third-party script AFTER hydration
const script = document.createElement("script");
script.src = "https://third-party.com/widget.js";
script.async = true;
containerRef.current?.appendChild(script);
}, []);
return <div ref={containerRef} />;
}---
Anti-Pattern 7: Forgetting Keys on Lists Rendered Differently
What People Do
// BAD: Server sorts by relevance, client sorts by date
function ArticleList({ articles }: { articles: Article[] }): JSX.Element {
const sorted = articles.sort((a, b) =>
typeof window !== "undefined"
? b.date.getTime() - a.date.getTime() // Client: by date
: b.relevance - a.relevance // Server: by relevance
);
return (
<ul>
{sorted.map((article) => (
<li key={article.id}>{article.title}</li>
))}
</ul>
);
}Why It Fails
Even with stable keys, the ORDER of children differs, causing a hydration mismatch.
Correct Approach
ALWAYS use the same sort order on server and client for the initial render. Apply client-specific sorting in useEffect.
function ArticleList({ articles }: { articles: Article[] }): JSX.Element {
const [sortBy, setSortBy] = useState<"relevance" | "date">("relevance");
useEffect(() => {
setSortBy("date"); // Switch to client-preferred sort after hydration
}, []);
const sorted = [...articles].sort((a, b) =>
sortBy === "date"
? b.date.getTime() - a.date.getTime()
: b.relevance - a.relevance
);
return (
<ul>
{sorted.map((article) => (
<li key={article.id}>{article.title}</li>
))}
</ul>
);
}---
Summary: The Golden Rule
ALWAYS ensure the FIRST client render produces IDENTICAL output to the server render. Any client-specific adjustments MUST happen in useEffect, which runs AFTER hydration is complete.
The hydration contract: 1. Server renders HTML 2. Client's first render MUST produce the same HTML 3. useEffect runs AFTER hydration -- safe to diverge here 4. React applies the state update as a normal re-render
Hydration Error Examples with Fixes
Example 1: User Locale Formatting
Problem
Server renders with en-US locale, client has de-DE.
// BAD: Locale differs between server and client
function Price({ amount }: { amount: number }): JSX.Element {
return <span>{amount.toLocaleString()}</span>;
// Server: "1,000.00" | Client: "1.000,00"
}Fix
function Price({ amount }: { amount: number }): JSX.Element {
const [formatted, setFormatted] = useState<string>(
amount.toFixed(2) // Deterministic server fallback
);
useEffect(() => {
setFormatted(amount.toLocaleString());
}, [amount]);
return <span>{formatted}</span>;
}---
Example 2: localStorage-Based Theme
Problem
Server does not have access to localStorage.
// BAD: localStorage is undefined on server
function ThemeProvider({ children }: { children: React.ReactNode }): JSX.Element {
const theme = localStorage.getItem("theme") || "light";
return <div className={theme}>{children}</div>;
}Fix
function ThemeProvider({ children }: { children: React.ReactNode }): JSX.Element {
const [theme, setTheme] = useState<string>("light"); // Server default
useEffect(() => {
const saved = localStorage.getItem("theme");
if (saved) {
setTheme(saved);
}
}, []);
return <div className={theme}>{children}</div>;
}---
Example 3: Window Dimensions for Responsive Layout
Problem
Window dimensions do not exist on the server.
// BAD: window is undefined during SSR
function ResponsiveGrid({ children }: { children: React.ReactNode }): JSX.Element {
const cols = window.innerWidth > 1024 ? 3 : 1;
return <div style={{ gridTemplateColumns: `repeat(${cols}, 1fr)` }}>{children}</div>;
}Fix
function useWindowWidth(fallback: number = 1024): number {
const [width, setWidth] = useState<number>(fallback);
useEffect(() => {
const handleResize = (): void => setWidth(window.innerWidth);
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return width;
}
function ResponsiveGrid({ children }: { children: React.ReactNode }): JSX.Element {
const width = useWindowWidth();
const cols = width > 1024 ? 3 : 1;
return <div style={{ gridTemplateColumns: `repeat(${cols}, 1fr)` }}>{children}</div>;
}Note: The fallback value (1024) MUST produce the same layout as the server render. Choose a sensible default that matches your server-side assumption.
---
Example 4: Authentication-Dependent Navigation
Problem
Auth token exists in cookies on client but is not available during server render in the same way.
// BAD: Different auth state between server and client
function NavBar(): JSX.Element {
const user = getClientSideUser(); // null on server, User on client
return (
<nav>
{user ? (
<span>Welcome, {user.name}</span>
) : (
<a href="/login">Sign in</a>
)}
</nav>
);
}Fix
function NavBar(): JSX.Element {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
const currentUser = getClientSideUser();
setUser(currentUser);
}, []);
return (
<nav>
{user ? (
<span>Welcome, {user.name}</span>
) : (
<a href="/login">Sign in</a>
)}
</nav>
);
}ALWAYS default to the unauthenticated state on the server. The flash of unauthenticated content is preferable to a hydration mismatch.
---
Example 5: useId for Stable Identifiers
Problem
Dynamically generated IDs differ between server and client.
// BAD: Math.random produces different values
function FormField({ label }: { label: string }): JSX.Element {
const id = `field-${Math.random().toString(36).slice(2)}`;
return (
<>
<label htmlFor={id}>{label}</label>
<input id={id} />
</>
);
}Fix
import { useId } from "react";
function FormField({ label }: { label: string }): JSX.Element {
const id = useId();
return (
<>
<label htmlFor={id}>{label}</label>
<input id={id} />
</>
);
}useId generates the same ID on server and client. It is designed specifically for this purpose. Available in React 18+.
---
Example 6: HTML Nesting Violation
Problem
Browser auto-corrects invalid HTML before React hydrates.
// BAD: <div> is not valid inside <p>
function Article({ content }: { content: string }): JSX.Element {
return (
<p>
<div className="highlight">{content}</div>
</p>
);
}Fix
// GOOD: Use <span> inside <p>, or <div> wrapping <p>
function Article({ content }: { content: string }): JSX.Element {
return (
<p>
<span className="highlight">{content}</span>
</p>
);
}
// Or restructure:
function Article({ content }: { content: string }): JSX.Element {
return (
<div>
<div className="highlight">{content}</div>
</div>
);
}---
Example 7: Dynamic Import (Next.js) for Client-Only Components
Problem
A map library requires window and cannot render on the server.
// BAD: MapContainer accesses window internally
import { MapContainer, TileLayer } from "react-leaflet";
function LocationMap(): JSX.Element {
return (
<MapContainer center={[51.505, -0.09]} zoom={13}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
</MapContainer>
);
}Fix (Next.js)
import dynamic from "next/dynamic";
const LocationMap = dynamic(
() => import("../components/LocationMap"),
{
ssr: false,
loading: () => (
<div style={{ height: 400, background: "#eee" }}>Loading map...</div>
),
}
);
// Use normally in pages/components
function Page(): JSX.Element {
return <LocationMap />;
}---
Example 8: Timestamps with suppressHydrationWarning
Problem
Relative timestamps like "3 minutes ago" always differ between server render time and client hydration time.
// Acceptable use of suppressHydrationWarning
function Comment({ createdAt }: { createdAt: string }): JSX.Element {
return (
<article>
<p>{comment.text}</p>
<time suppressHydrationWarning dateTime={createdAt}>
{formatRelativeTime(createdAt)}
</time>
</article>
);
}This is one of the FEW legitimate uses of suppressHydrationWarning. The timestamp will correct itself on the client, and the brief mismatch is acceptable.
---
Example 9: hydrateRoot with Error Tracking
Production Setup
import { hydrateRoot } from "react-dom/client";
import { reportError } from "./errorTracking";
import App from "./App";
const EXTENSION_PATTERNS = [
/data-grammarly/,
/data-lastpass/,
/data-dashlanecreated/,
];
function isExtensionError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error);
return EXTENSION_PATTERNS.some((pattern) => pattern.test(message));
}
hydrateRoot(
document.getElementById("root") as HTMLElement,
<App />,
{
onRecoverableError(error: unknown, errorInfo: { componentStack?: string }) {
if (isExtensionError(error)) {
return; // Ignore extension-caused mismatches
}
reportError({
type: "hydration-mismatch",
error,
componentStack: errorInfo.componentStack,
});
},
}
);ALWAYS filter known extension patterns in onRecoverableError to reduce noise in production error tracking.