A hydration mismatch happens when the HTML React generates in the browser during hydration doesn't match the HTML the server actually sent — React detects the difference and, depending on severity, either patches it (with a console warning) or throws outright. The usual root causes:
// WRONG — Date.now() produces a different value on the server (when the
// HTML was generated) than in the browser (a moment later, during hydration)
export default function Timestamp() {
return <p>Rendered at: {Date.now()}</p>;
}
// WRONG — same problem, Math.random() can't produce the same value twice
export default function RandomId() {
return <div id={`item-${Math.random()}`} />;
}
The fix is computing anything non-deterministic in a useEffect (which only runs in the browser, after hydration) instead of directly in the render body, so the initial server-rendered HTML and the first client render genuinely match, and the true, live value fills in a beat later:
"use client";
import { useState, useEffect } from "react";
export default function Timestamp() {
const [time, setTime] = useState<number | null>(null);
useEffect(() => setTime(Date.now()), []); // runs client-side only, after hydration
return <p>Rendered at: {time ?? "…"}</p>;
}
Two other common sources of the same symptom:
window, localStorage, or document referenced directly in a component body that also has to run on the server, where none of those globals exist at all. This usually throws a hard error during server rendering rather than a silent mismatch, but the fix is the same: move the access into useEffect, or guard it with a check for whether window exists.// WRONG — this is a Server Component (no "use client"), and useState
// simply doesn't exist in a context that never re-renders in a browser
export default function Counter() {
const [count, setCount] = useState(0); // build error: useState only works in Client Components
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Any hook that depends on interactivity or a browser environment — useState, useEffect, useContext (for most uses), event handlers like onClick — requires "use client" at the top of that file. Next.js will refuse to build rather than let this fail silently, but the fix is worth internalizing rather than treating as a rule to memorize by rote: those hooks only mean something in a place that re-renders in a browser in response to user interaction, and a Server Component, by definition, never does that.
NEXT_PUBLIC_ prefix rule# .env.local
DATABASE_URL=postgres://... # server-only — never reaches the browser
NEXT_PUBLIC_ANALYTICS_ID=UA-12345 # bundled into client JavaScript, visible to anyone
"use client";
export default function Analytics() {
console.log(process.env.DATABASE_URL); // undefined in the browser — silently
console.log(process.env.NEXT_PUBLIC_ANALYTICS_ID); // "UA-12345" — actually available
}
An environment variable is only inlined into client-side JavaScript if its name is prefixed with NEXT_PUBLIC_. Anything else is available in server-side code (a Server Component, a Route Handler, a Server Action) but resolves to undefined the moment you read it from code that runs in the browser — no error, no warning, just a silently missing value that's easy to mistake for a bug in whatever's consuming it.
This is a deliberate security boundary, not an oversight. A database connection string, an API secret key, a signing secret — none of that should ever be readable from a browser's DevTools, and requiring an explicit NEXT_PUBLIC_ prefix means leaking a secret into the client bundle takes an active, visible choice (typing that exact prefix) rather than an accident of an environment variable simply existing. Treat any variable without that prefix as a promise to yourself that it will never show up in anything sent to a browser — and treat "why is this env var undefined on the client" as a signal to check for the missing prefix before assuming anything else is broken.
Server Components, the fetch caching layer, and Server Actions are the three ideas that make Next.js feel like a genuinely different framework from "React with a router bolted on" — see React Fundamentals for the underlying component model all of this still builds on. Try wiring up a small App Router project — a couple of nested layouts, one Server Action, one revalidated fetch — in the code lab.