You asked your AI coding agent for a greeting that shows the visitor’s saved name, or a “posted at” time, or a dark mode toggle. It works in the browser, then the console fills with a red error: “Text content does not match server-rendered HTML”, or “Hydration failed because the server rendered HTML didn’t match the client”. The page may flicker, or a chunk of the UI may be thrown away and rebuilt.
The code looks fine because the bug is not a typo. It is a value that is different in two places.
Why this happens
In a framework like Next.js, your component renders twice. First on the server, which produces the HTML you see before any JavaScript loads. Then in the browser, where React runs the same component and attaches event handlers to that HTML. The Next.js docs call this step hydration, and the error means that the tree from the server and the tree from the first browser render were not the same.
The server has no window, no localStorage, and a different clock than your visitor. So any value that depends on those produces different output in each place. The Next.js docs list the common causes, and agents write several of them by default:
- Browser-only APIs such as
windoworlocalStoragein the render logic typeof window !== 'undefined'checks in the render logic- Time-dependent APIs such as
new Date()in the render logic - Invalid HTML nesting, such as a
<div>or<ul>inside a<p>, or a<button>inside a<button> - Browser extensions that modify the HTML before React sees it
Agents reach for the first three because the code reads naturally:
function Welcome() {
const name = localStorage.getItem("name") ?? "friend" // no localStorage on the server
return <h1>Hello, {name}. It is {new Date().toLocaleTimeString()}</h1>
}
The server renders “Hello, friend” with the server’s time. The browser renders the saved name with the visitor’s time. Two different trees, one error.
The typeof window check does not rescue you. It just makes the two renders take different branches, which is the mismatch itself.
How to tell if this is your problem
- Read the error’s diff. In development, the message shows what the server rendered next to what the client rendered. The differing text or element points at the component.
- Search the component for
window,localStorage,document,Date, andMath.randomused while rendering, outside of an effect or event handler. - Check your HTML nesting. A
<p>that contains a<div>, a list, or another<p>is invalid, and browsers repair it in a way that no longer matches what React expected. - Rule out extensions. Open the page in a private window with extensions disabled. If the error disappears, an extension is rewriting the HTML and your code is fine.
The fix
Make the first client render identical to the server render, then switch to the browser-only value after hydration. The Next.js docs show the pattern: render a placeholder first, then update inside useEffect, which only runs in the browser.
import { useState, useEffect } from "react"
function Welcome() {
const [name, setName] = useState("friend")
const [time, setTime] = useState(null)
useEffect(() => {
setName(localStorage.getItem("name") ?? "friend")
setTime(new Date().toLocaleTimeString())
}, [])
return <h1>Hello, {name}.{time && ` It is ${time}`}</h1>
}
Two other options from the same docs, for narrower cases:
- Skip server rendering for one component. Load it with
dynamic(() => import("./Widget"), { ssr: false }). Use this for something that is meaningless without the browser, like a chart that measures its container. - Silence an unavoidable difference. For a value that will always differ, such as a timestamp, put
suppressHydrationWarningon that one element. The docs say it only works one level deep, that it is an escape hatch, and that React will not try to patch mismatched text when it is set. Do not spread it across your layout to make the console quiet.
For the nesting case there is no flag: change the markup. Swap the outer <p> for a <div>, or move the block-level element out of the paragraph.
How to avoid this next time
When you ask an agent for anything involving the visitor’s saved settings, the current time, or random values, add one line to the prompt: “this component is server-rendered, so the first render must not read window, localStorage, or the clock; set those in useEffect.” Then in review, scan each component for those APIs appearing in the render body rather than in an effect. That catches nearly every hydration error before it ships.
Comments
Sign in to join the conversation.
No comments yet — be the first.