Hydration Proof

Search documentation

Find a page or section

Fix server/client data mismatch hydration errors

The client fetched again, and the answer had changed.

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 renders process.env.REGION gets 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:

app/stats/page.tsx
import { Visits } from "./visits";
 
export default async function Page() {
  const stats = await getStats();
  return <Visits initialCount={stats.count} />;
}
app/stats/visits.tsx
"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:

app/posts/page.tsx
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.