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.
Install
npx skillscat add ethanniser/hydration-test/hydration-safe-inputs Install via the SkillsCat registry.
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:
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 (
<input
id="my-input"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}Key Requirements
- Element needs an
id- The initializer must find the element - Use lazy initializer -
useState(() => ...)notuseState(...) - Guard for SSR - Check
typeof window !== "undefined" - Type check the element - Use
instanceof HTMLInputElement(orHTMLTextAreaElement,HTMLSelectElement)
Patterns by Input Type
Text Input
const [value, setValue] = useState(() => {
if (typeof window !== "undefined") {
const el = document.getElementById("my-input");
if (el instanceof HTMLInputElement) return el.value;
}
return "";
});Checkbox
const [checked, setChecked] = useState(() => {
if (typeof window !== "undefined") {
const el = document.getElementById("my-checkbox");
if (el instanceof HTMLInputElement) return el.checked;
}
return false;
});Select
const [value, setValue] = useState(() => {
if (typeof window !== "undefined") {
const el = document.getElementById("my-select");
if (el instanceof HTMLSelectElement) return el.value;
}
return "default";
});Textarea
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:
Controlled inputs with hardcoded initial state:
// VULNERABLE const [value, setValue] = useState(""); return <input value={value} onChange={...} />;Form components in SSR/SSG pages - Any
"use client"component with controlled inputs in Next.js App Router, or any component inpages/directoryComponents without hydration-safe initialization - Missing the
typeof windowguard pattern
Refactoring Checklist
When fixing an existing component:
- Add unique
idto the input element - Replace
useState(defaultValue)withuseState(() => { ... }) - Add window check and DOM query in initializer
- Add appropriate
instanceoftype guard - Keep original default as SSR fallback
Custom Hook (Optional)
For apps with many inputs, extract to a reusable hook:
function useHydrationSafeValue<T>(
id: string,
defaultValue: T,
extract: (el: Element) => T
): [T, React.Dispatch<React.SetStateAction<T>>] {
const [value, setValue] = useState<T>(() => {
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:
- Add artificial hydration delay (slow network or blocking script)
- Type into the input before hydration completes
- Confirm input value persists after hydration
Example delay script for testing:
// Add to layout to simulate slow hydration
<script src="/api/slow-script" /> // endpoint that delays 2-3 seconds