A server/client data mismatch hydration error happens when the first client render uses different data than the server did: the client fetched again and got a newer answer, read a different cache, or evaluated a flag on its own. Send the exact data the server rendered with to the client, and hydrate from it before fetching anything new.
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 a
list that is longer or shorter on one side as
HP1007, HP1008 or
HP1009, with the cause Server and client used different
data. This is the hardest cause to read off the values (68% in the package's
test suite): a counter one higher on the client and a fetch near the element
are the clues. A probe gives the proof.
Why a server/client data mismatch hydration error happens
The server renders with the data it had at that moment. If the client does not receive that exact data, its first render works from something else:
- 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 App Router, fetch in a Server Component and pass the result to the Client Component. React serializes the props into the page, so the client renders the same data:
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>;
}Refresh the value after hydration (polling, a subscription, an effect) if it must stay live. The first render uses the snapshot; later renders can use newer data.
Hydrate the query cache
With TanStack Query, prefetch on the server and send the cache along with
dehydrate and HydrationBoundary. The client's useQuery then starts with
the server's data instead of fetching:
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>
);
}Set a default staleTime above 0, as the
TanStack Query SSR guide
recommends, so the client does not refetch as soon as it hydrates. For a
single query, initialData from a server prop does the same job.
Use the framework's loader data
React Router and Remix serialize loader data into the page. Read it with
useLoaderData() in the component instead of fetching again in an effect or
during render, and both sides render from the same snapshot.
Decide flags once, on the server
Evaluate feature flags and experiment buckets on the server, and pass the result down (a prop, a context value, a cookie the server set). The client reads the decision; it never makes its own.
Catch it with ESLint
No lint rule can tell whether two fetches return the same data, so there is no
rule for this cause. hydration-proof test finds it in the running app.
Catch it in CI
hydration-proof test reports the differing text or elements. With --probe,
it reloads the page with nothing changed at all: if that identical reload
renders a different value, the page depends on server data, not on the clock,
the locale or anything else in the browser. See probes.
npx hydration-proof test --probe --repeat 3--repeat loads each page several times and marks findings that come and go
as flaky, which data mismatches often are. To make the client's requests
predictable in a test, a scenario's mocks answer browser requests; requests
the server makes are not affected. See scenarios.