A ClientOnly Next.js component shows a fallback on the server and during the first browser render. It shows its children only after hydration, so the two initial renders match. Use it for a map, chart or other UI that needs browser APIs while rendering.
Build a ClientOnly Next.js component
useSyncExternalStore returns the server snapshot during server rendering
and hydration. It then switches to the browser snapshot. Use that change to
show the fallback first and the children later:
"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>
);
}You can also start with useState(false) and set it to true in an effect.
See two-pass rendering for that
approach.
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" alone does not stop server rendering. Next.js still makes HTML
for Client Components before hydrating them. To delay a component until the
browser is ready, choose one of these approaches:
| 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 provides ClientOnly
for React Router and Remix. Its child is a function that runs 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>
);
}The fallback stays in place through the first browser render. Afterwards,
the component shows its child. For smaller differences, use the package's
useHydrated hook directly. See Remix hydration errors.
ClientOnly TanStack Router and Start
TanStack Router also exports
ClientOnly.
Pass a fallback for the server and first browser render:
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?
You cannot keep a Client Component's server HTML and turn off hydration for that component. You can decide which parts of the page need client code:
- 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.
Run hydration-proof to check the result. A properly delayed component has no hydration finding. If another part still renders differently, the report shows the element and both values.
npx hydration-proof test