---
name: hydration-safe-inputs
description: Fix React hydration issues where user input typed before hydration gets wiped/cleared when React takes over. Use when (1) users report input fields clearing on page load, (2) working with SSR/SSG React apps (Next.js, Remix, Astro) that have controlled inputs, (3) auditing forms for hydration safety, or (4) building new forms in statically rendered React apps.
---
# Hydration-Safe Inputs
## The Problem
In SSR/SSG React apps, there's a window between when HTML renders and when React hydrates. If a user types into an input during this window, React's hydration will wipe their input because React initializes state to the default value (usually empty string).
```
Timeline:
1. HTML arrives → input rendered (empty)
2. User types "hello" → input shows "hello"
3. React hydrates → useState("") runs → input wiped to ""
```
## The Fix
Initialize state by reading the DOM element's current value instead of a hardcoded default:
```tsx
function Input() {
const [value, setValue] = useState(() => {
if (typeof window !== "undefined") {
const input = document.getElementById("my-input");
if (input instanceof HTMLInputElement) {
return input.value;
}
}
return ""; // server fallback
});
return (
setValue(e.target.value)}
/>
);
}
```
## Key Requirements
1. **Element needs an `id`** - The initializer must find the element
2. **Use lazy initializer** - `useState(() => ...)` not `useState(...)`
3. **Guard for SSR** - Check `typeof window !== "undefined"`
4. **Type check the element** - Use `instanceof HTMLInputElement` (or `HTMLTextAreaElement`, `HTMLSelectElement`)
## Patterns by Input Type
### Text Input
```tsx
const [value, setValue] = useState(() => {
if (typeof window !== "undefined") {
const el = document.getElementById("my-input");
if (el instanceof HTMLInputElement) return el.value;
}
return "";
});
```
### Checkbox
```tsx
const [checked, setChecked] = useState(() => {
if (typeof window !== "undefined") {
const el = document.getElementById("my-checkbox");
if (el instanceof HTMLInputElement) return el.checked;
}
return false;
});
```
### Select
```tsx
const [value, setValue] = useState(() => {
if (typeof window !== "undefined") {
const el = document.getElementById("my-select");
if (el instanceof HTMLSelectElement) return el.value;
}
return "default";
});
```
### Textarea
```tsx
const [value, setValue] = useState(() => {
if (typeof window !== "undefined") {
const el = document.getElementById("my-textarea");
if (el instanceof HTMLTextAreaElement) return el.value;
}
return "";
});
```
## Identifying Vulnerable Components
Search for these patterns that indicate potential hydration wipe issues:
1. **Controlled inputs with hardcoded initial state:**
```tsx
// VULNERABLE
const [value, setValue] = useState("");
return ;
```
2. **Form components in SSR/SSG pages** - Any `"use client"` component with controlled inputs in Next.js App Router, or any component in `pages/` directory
3. **Components without hydration-safe initialization** - Missing the `typeof window` guard pattern
## Refactoring Checklist
When fixing an existing component:
1. Add unique `id` to the input element
2. Replace `useState(defaultValue)` with `useState(() => { ... })`
3. Add window check and DOM query in initializer
4. Add appropriate `instanceof` type guard
5. Keep original default as SSR fallback
## Custom Hook (Optional)
For apps with many inputs, extract to a reusable hook:
```tsx
function useHydrationSafeValue(
id: string,
defaultValue: T,
extract: (el: Element) => T
): [T, React.Dispatch>] {
const [value, setValue] = useState(() => {
if (typeof window !== "undefined") {
const el = document.getElementById(id);
if (el) return extract(el);
}
return defaultValue;
});
return [value, setValue];
}
// Usage:
const [value, setValue] = useHydrationSafeValue(
"my-input",
"",
(el) => (el as HTMLInputElement).value
);
```
## Testing
To verify the fix works:
1. Add artificial hydration delay (slow network or blocking script)
2. Type into the input before hydration completes
3. Confirm input value persists after hydration
Example delay script for testing:
```tsx
// Add to layout to simulate slow hydration
// endpoint that delays 2-3 seconds
```