xiaohongshu
xiaohongshu
hydration-safety
Next.js SSR hydration patterns and best practices. Use when fixing hydration mismatches, implementing client-only features, or working with localStorage/browser APIs.
Full skill instructions
Next.js uses Server-Side Rendering (SSR), which means components render twice:
Mismatches between these two renders cause hydration errors. This skill covers patterns to prevent and fix these issues.
// ❌ WRONG - Causes hydration mismatch
function Component() {
const user = localStorage.getItem("user"); // localStorage not available on server
return <div>Welcome {user}</div>;
}
```typescript
**Error:** "Text content does not match server-rendered HTML"
**Why:** Server renders `<div>Welcome </div>`, client tries to render `<div>Welcome John</div>`
## The isMounted Pattern (CRITICAL)
This is the **standard solution** for hydration-safe components:
```typescript
"use client";
import { useState, useEffect } from "react";
function Component() {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
// Return null or skeleton during SSR
if (!isMounted) {
return null; // or <Skeleton />
}
// Only runs on client after hydration
const user = localStorage.getItem("user");
return <div>Welcome {user}</div>;
}
```typescript
### How It Works
1. **Server-side:** `isMounted = false`, component returns `null`
2. **Client-side (first render):** `isMounted = false`, still returns `null`
3. **Client-side (after useEffect):** `isMounted = true`, renders with browser APIs
4. **Result:** Server and first client render match perfectly
## Common Hydration Triggers
### 1. Browser APIs
❌ **Causes Hydration Errors:**
- `localStorage`
- `sessionStorage`
- `window`
- `document`
- `navigator`
- Date/time without consistent timezone
✅ **Solution:** Use isMounted pattern
### 2. Random Values
```typescript
// ❌ WRONG
function Component() {
const id = Math.random(); // Different on server vs client
return <div id={id}>Content</div>;
}
// ✅ CORRECT
function Component() {
const [id, setId] = useState<string>();
useEffect(() => {
setId(String(Math.random()));
}, []);
return <div id={id}>Content</div>;
}
```typescript
### 3. Date/Time
```typescript
// ❌ WRONG
function Clock() {
return <div>{new Date().toLocaleTimeString()}</div>;
}
// ✅ CORRECT
"use client";
function Clock() {
const [time, setTime] = useState<string>();
useEffect(() => {
setTime(new Date().toLocaleTimeString());
const interval = setInterval(() => {
setTime(new Date().toLocaleTimeString());
}, 1000);
return () => clearInterval(interval);
}, []);
if (!time) return <div>--:--:--</div>; // Placeholder
return <div>{time}</div>;
}
```typescript
## Implementation Patterns
### Pattern 1: Null During SSR
```typescript
"use client";
function Component() {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
if (!isMounted) return null;
return <div>{/* Browser-dependent content */}</div>;
}
```typescript
**Use when:** Component doesn't need to show during SSR
### Pattern 2: Skeleton During SSR
```typescript
"use client";
function Component() {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
if (!isMounted) {
return <div className="animate-pulse bg-surf-1 h-10 w-32" />;
}
return <div>{/* Actual content */}</div>;
}
```typescript
**Use when:** Need loading state visible during SSR
### Pattern 3: Safe Defaults
```typescript
"use client";
function Component() {
const [data, setData] = useState<string>("default");
useEffect(() => {
const stored = localStorage.getItem("key");
if (stored) setData(stored);
}, []);
return <div>{data}</div>;
}
```typescript
**Use when:** Can show default value during SSR
## Project-Specific Patterns
### Sidebar Assistant (Example from codebase)
Location: `src/components/chat/chat-sidebar.tsx`
```typescript
"use client";
export function ChatSidebar() {
const [isMounted, setIsMounted] = useState(false);
const { isPinned, width } = useChatSidebar();
useEffect(() => {
setIsMounted(true);
}, []);
if (!isMounted) {
return null; // Sidebar hidden during SSR
}
return (
<div style={{ width: `${width}px` }}>
{/* Sidebar content */}
</div>
);
}
```typescript
### Global Chat Button
Location: `src/components/global-chat-button.tsx`
```typescript
"use client";
export function GlobalChatButton() {
const { isOpen } = useChatSidebar();
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, [isOpen]); // Added isOpen to dependency array
// Early return AFTER all hooks
if (!isMounted || isOpen) {
return null;
}
return <button>Open Chat</button>;
}
```typescript
**Note:** All hooks must be called before conditional returns!
## Testing Hydration Safety
### Unit Tests
```typescript
import { render, screen } from "@testing-library/react";
import { act } from "react";
describe("Hydrated Component", () => {
it("should not show content during SSR", () => {
render(<Component />);
expect(screen.queryByTestId("hydrated-content")).not.toBeInTheDocument();
});
it("should show content after mount", async () => {
render(<Component />);
// Wait for useEffect
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
expect(screen.getByTestId("hydrated-content")).toBeInTheDocument();
});
});
```typescript
### E2E Tests (Playwright)
```typescript
import { test, expect } from "@playwright/test";
test("should handle hydration correctly", async ({ page }) => {
await page.goto("/", { waitUntil: "networkidle" });
// Wait for React hydration to complete
await page.waitForSelector('[data-testid="hydrated-component"]', {
state: "attached",
timeout: 10000,
});
// Wait for loading spinner to disappear (if present)
await page.waitForSelector(".animate-spin", {
state: "detached",
timeout: 10000,
});
// Additional stabilization time
await page.waitForTimeout(500);
// Now safe to interact
await page.click('[data-testid="interactive-element"]');
});
```typescript
### Why E2E Waits Are Critical
Without waits, E2E tests run on intermediate DOM states:
```typescript
// ❌ WRONG - Runs too early
test("wrong timing", async ({ page }) => {
await page.goto("/");
await page.click("button"); // May not be hydrated yet!
});
// ✅ CORRECT - Waits for hydration
test("correct timing", async ({ page }) => {
await page.goto("/", { waitUntil: "networkidle" });
await page.waitForSelector('[data-testid="ready"]');
await page.waitForTimeout(500); // DOM stabilization
await page.click("button"); // Now safe!
});
```typescript
## Debugging Hydration Errors
### Error Messages
**"Text content does not match server-rendered HTML"**
- Cause: Different text rendered on server vs client
- Solution: Use isMounted pattern or ensure consistent data
**"Hydration failed because the initial UI does not match"**
- Cause: Different structure rendered on server vs client
- Solution: Make server and client render the same initially
**"There was an error while hydrating"**
- Cause: Component error during hydration
- Solution: Check browser console for specific error
### Debugging Steps
1. **Check Browser Console** - Specific error details appear here
2. **Inspect Network Tab** - Compare SSR HTML vs hydrated DOM
3. **Use React DevTools** - Highlight hydration mismatches
4. **Add data-testid After Mount** - Verify component is hydrated
```typescript
return (
<div data-testid={isMounted ? "hydrated" : undefined}>
Content
</div>
);
```typescript
5. **Check localStorage/sessionStorage Access** - Most common cause
## React Hooks Rules with Hydration
### ⚠️ Critical: Hooks Before Returns
```typescript
// ❌ WRONG - Hook after conditional return
function Component() {
const { isOpen } = useContext();
if (!isOpen) return null; // Early return
useEffect(() => { // ❌ Hook after return!
// ...
}, []);
}
// ✅ CORRECT - All hooks before returns
function Component() {
const { isOpen } = useContext();
const [isMounted, setIsMounted] = useState(false);
useEffect(() => { // ✓ Hook before return
setIsMounted(true);
}, []);
if (!isOpen || !isMounted) return null; // After all hooks
}
```typescript
## Common Mistakes
### Mistake 1: Checking window Directly
```typescript
// ❌ WRONG
function Component() {
if (typeof window === "undefined") return null;
return <div>{localStorage.getItem("key")}</div>;
}
```typescript
**Problem:** First client render still won't match server
### Mistake 2: Forgetting "use client"
```typescript
// ❌ WRONG - Missing directive
import { useState, useEffect } from "react";
function Component() {
const [mounted, setMounted] = useState(false);
// ...
}
```typescript
**Problem:** Server components can't use hooks
### Mistake 3: Using useLayoutEffect
```typescript
// ❌ WRONG - Causes SSR warnings
useLayoutEffect(() => {
setIsMounted(true);
}, []);
// ✅ CORRECT - Use useEffect for hydration
useEffect(() => {
setIsMounted(true);
}, []);
```typescript
## Checklist for Hydration-Safe Components
- [ ] Add "use client" directive if using hooks
- [ ] All hooks called before conditional returns
- [ ] Use isMounted pattern for browser APIs
- [ ] Return consistent structure during SSR and first client render
- [ ] Add data-testid after mount for E2E tests
- [ ] Test component in both SSR and client contexts
- [ ] Verify no hydration errors in browser console
- [ ] E2E tests wait for hydration before interactions
## Quick Reference
```typescript
// Standard isMounted pattern
const [isMounted, setIsMounted] = useState(false);
useEffect(() => { setIsMounted(true); }, []);
if (!isMounted) return null;
// E2E wait pattern
await page.goto("/", { waitUntil: "networkidle" });
await page.waitForSelector('[data-testid="ready"]');
await page.waitForTimeout(500);
// Debug in browser
// Check: document.documentElement.dataset.reactHydrated
```typescript
## Related Files
- `src/components/chat/chat-sidebar.tsx` - Production example
- `src/components/global-chat-button.tsx` - Production example
- `e2e/a11y.spec.ts` - E2E hydration patterns
- `src/lib/chat-sidebar-context.tsx` - Context with hydration safety
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience