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:
"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;
}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.
ClientOnlystops the child from rendering there, not from being imported. A library that toucheswindowwhen it is imported needsnext/dynamicwithssr: falseinstead. - 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:
| Option | Server renders | Code is loaded | Use it for |
|---|---|---|---|
ClientOnly wrapper | The fallback | With the page | Components that read the browser while rendering |
next/dynamic with ssr: false | The loading fallback | Separately, in the browser | Libraries that need window when imported, large widgets |
| Two-pass rendering in the component | Its server version | With the page | One 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:
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:
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
ClientOnlywrapper ornext/dynamicwithssr: 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