Hydration Proof

Search documentation

Find a page or section

Render a fallback on the server, the real thing after hydration.

A ClientOnly Next.js component renders a fallback on the server and during hydration, then its children once React runs in the browser. Both renders that React compares show the fallback, so there is no mismatch. Use it for parts that cannot render on the server: a map, a chart, or anything that depends on the window.

Build a ClientOnly Next.js component

useSyncExternalStore returns the server snapshot on the server and during hydration, and the client snapshot afterwards. That is exactly the switch a client-only wrapper needs:

components/client-only.tsx
"use client";
 
import { useSyncExternalStore, type ReactNode } from "react";
 
const subscribe = () => () => {};
 
export function ClientOnly({
  children,
  fallback = null,
}: {
  children: ReactNode;
  fallback?: ReactNode;
}) {
  const isClient = useSyncExternalStore(
    subscribe,
    () => true, // in the browser, after hydration
    () => false // on the server and during hydration
  );
  return isClient ? children : fallback;
}
app/stores/page.tsx
import { ClientOnly } from "@/components/client-only";
import { StoreMap } from "./store-map";
 
export default function Page() {
  return (
    <ClientOnly fallback={<div className="h-96 rounded bg-muted" />}>
      <StoreMap />
    </ClientOnly>
  );
}

The same wrapper with useEffect works too: start with useState(false) and set it to true in an effect. React's docs describe that version as two-pass rendering.

Two things to keep in mind:

  • The child's module still loads on the server. ClientOnly stops the child from rendering there, not from being imported. A library that touches window when it is imported needs next/dynamic with ssr: false instead.
  • Give the fallback the final size. Otherwise the content jumps when the real component replaces it.

How to render components in Next.js only on the client

"use client" is not enough: Client Components are still prerendered to HTML on the server, then hydrated. To keep a component out of the server render, pick one of these:

OptionServer rendersCode is loadedUse it for
ClientOnly wrapperThe fallbackWith the pageComponents that read the browser while rendering
next/dynamic with ssr: falseThe loading fallbackSeparately, in the browserLibraries that need window when imported, large widgets
Two-pass rendering in the componentIts server versionWith the pageOne value that differs, such as a stored preference

ClientOnly Remix and React Router: remix-utils

remix-utils ships a ClientOnly component for React Router and Remix apps. Its children are a function, so the component inside is only created in the browser:

app/routes/dashboard.tsx
import { ClientOnly } from "remix-utils/client-only";
import { Chart } from "~/components/chart";
 
export default function Dashboard() {
  return (
    <ClientOnly fallback={<div className="h-64" />}>
      {() => <Chart />}
    </ClientOnly>
  );
}

Its README describes the flow: on the server and on the first client render it always renders the fallback, then it updates to the real component, and later renders skip the fallback. It is built on the package's useHydrated hook, which you can use directly for smaller differences. More on the framework in Remix hydration errors.

ClientOnly TanStack Router and Start

TanStack Router exports its own ClientOnly, with a fallback prop for the render before JavaScript loads:

src/routes/dashboard.tsx
import { ClientOnly, createFileRoute } from "@tanstack/react-router";
import { Charts, FallbackCharts } from "~/components/charts";
 
export const Route = createFileRoute("/dashboard")({
  component: Dashboard,
});
 
function Dashboard() {
  return (
    <ClientOnly fallback={<FallbackCharts />}>
      <Charts />
    </ClientOnly>
  );
}

See TanStack Start for testing those routes.

Can you disable hydration or only partially hydrate a Next.js app?

There is no switch that disables hydration for a Client Component while keeping its server HTML. What you can do is decide which parts need hydrating at all:

  • Keep static parts as Server Components. Their code never runs in the browser, and nothing about them needs hydrating except the plain HTML elements. This is how the App Router lets you only partially hydrate a Next.js app.
  • Mark only the interactive leaves "use client". A button or a form hydrates; the article around it does not ship its code.
  • Render browser-only parts on the client with a ClientOnly wrapper or next/dynamic with ssr: false.
  • Split the page with Suspense boundaries. Each boundary hydrates on its own, and a mismatch inside one re-renders only that boundary.

What client-only rendering costs

  • No HTML for that part. Crawlers, link previews and users without JavaScript see the fallback.
  • It appears later. The content waits for hydration, or for its own chunk to load.
  • Layout shift, unless the fallback has the size of the final content.

hydration-proof checks the result: a correct client-only part produces no finding, while a component that still renders differently on the server is reported with the element and both values.

npx hydration-proof test