A server/client data mismatch hydration error means the server and first browser render used different data. The browser may have fetched again, started with an empty cache, or made a different feature-flag decision. Send the server's data with the page and use it for the first browser render.
Symptoms
Hydration failed because the server rendered text didn't match the client.
Warning: Text content did not match. Server: "Visits: 41" Client: "Visits: 42"
Warning: Expected server HTML to contain a matching <li> in <ul>.
Warning: Did not expect server HTML to contain a <li> in <ul>.React 19's own message names this cause: "External changing data without sending a snapshot of it along with the HTML."
hydration-proof reports changed text as HP1001 and added or missing elements as HP1007, HP1008, or HP1009. A changed counter or a fetch near the element can point to data as the cause.
Why a server/client data mismatch hydration error happens
The first browser render must use the same snapshot as the server. These patterns break that agreement:
- A second fetch. A Client Component fetches during render (with a suspense-enabled hook or a cache that was never filled on the client) and the data changed in between: a view counter, a stock level, a feed.
- A cache that was not sent. The server filled a query cache, but the client starts with an empty one and renders loading state or refetched data.
- Decisions made twice. A feature flag, an A/B test bucket or a personalization rule is evaluated on the server and again in the browser.
- Environment values. In Next.js, environment variables without the
NEXT_PUBLIC_prefix are only available on the server, so a Client Component that rendersprocess.env.REGIONgets a value on the server and nothing in the browser.
How to fix it
Pass the server's data as props
In the Next.js App Router, fetch in a Server Component and pass the result to the Client Component:
import { Visits } from "./visits";
export default async function Page() {
const stats = await getStats();
return <Visits initialCount={stats.count} />;
}"use client";
import { useState } from "react";
export function Visits({ initialCount }: { initialCount: number }) {
// Before: const { count } = use(fetchStats()) fetched again on the client
const [count] = useState(initialCount);
return <p>Visits: {count}</p>;
}If the value must stay current, refresh it after hydration. Only the first render has to use the server snapshot.
Hydrate the query cache
With TanStack Query, prefetch on the server and pass the cache through
dehydrate and HydrationBoundary. The browser's query starts with that data:
import {
dehydrate,
HydrationBoundary,
QueryClient,
} from "@tanstack/react-query";
import { Posts } from "./posts";
export default async function PostsPage() {
const queryClient = new QueryClient();
await queryClient.prefetchQuery({ queryKey: ["posts"], queryFn: getPosts });
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<Posts />
</HydrationBoundary>
);
}Consider a nonzero staleTime so the browser does not refetch immediately
(TanStack Query SSR guide).
For one query, a server prop passed as initialData can also work.
Use the framework's loader data
In React Router or Remix, read serialized loader data with useLoaderData().
Do not fetch the same value again for the first browser render.
Decide flags once, on the server
Choose a feature flag or experiment bucket once on the server. Pass the decision to the browser through a prop, context, or cookie.
Catch it with ESLint
A linter cannot know whether two requests return the same result. Check the
running page with hydration-proof test.
Catch it in CI
With --probe, hydration-proof tries an identical reload. If the value still
changes, inspect changing server or API data. See probes.
npx hydration-proof test --probe --repeat 3--repeat shows whether the problem comes and goes. To control browser
requests in a test, use a scenario's mocks. Server requests are not changed
by those mocks. See scenarios.