React
server-only and client-only: the imports that break the build
Two one-line packages that turn a wrong import into a build error. Where to put them, what they block and what they leave unprotected.
Key points
- `server-only` blocks a server module from being imported into the client graph.
- `client-only` does the opposite for modules that read the browser.
- The `"use client"` and `"use server"` directives declare intent; they do not protect anything.
- They protect the module, not the value: a secret passed as a prop still leaves.
`server-only` and `client-only` turn a wrong import into a build error. They do not change the runtime: they stop the bundler from placing a module in a graph it does not belong to.
How to install them
npm install server-only client-only
They are two empty packages with a conditional `exports` field: the `react-server` condition resolves to a file that throws. The bundler does the rest, and it runs in CI too.
The classic case: a client importing the database
// lib/db.ts
import "server-only";
import { PrismaClient } from "@prisma/client";
const globalForDb = globalThis as unknown as { db?: PrismaClient };
export const db = globalForDb.db ?? new PrismaClient();
if (process.env.NODE_ENV !== "production") globalForDb.db = db;
The first line is the whole protection. If a client component imports `lib/db.ts`, the build stops with a message naming the module and the path. Without that line the bundle would try to include the driver, and sometimes it succeeds.
// lib/seed.ts
import "server-only";
import { db } from "./db";
export async function seedDemo() {
const count = await db.user.count();
if (count > 0) return { skipped: true };
await db.user.createMany({ data: [{ email: "[email protected]" }] });
return { skipped: false };
}
Put the import at the top of every module that touches secrets, the filesystem, private environment variables or a database driver. Even in files that look harmless: a seed script imports the db, and sooner or later someone imports it from a page.
`client-only` is rarer, but it earns its place
// components/local-time.tsx
"use client";
import "client-only";
export function LocalTime({ iso }: { iso: string }) {
const formatted = new Intl.DateTimeFormat("it-IT", {
dateStyle: "full",
timeStyle: "short",
}).format(new Date(iso));
return {formatted};
}
A component that reads the time zone, `localStorage` or a browser API cannot render on the server: the result would differ between server and client and React would report a hydration mismatch.
Directives and packages are not the same thing
| Tool | What it declares | What it blocks |
|---|---|---|
| `"use client"` | A client module and its subtree | Nothing: the server may still import it |
| `"use server"` | An action function | Nothing: the client receives a reference |
| `server-only` | A module that lives on the server | The import from the client graph |
| `client-only` | A module that lives in the browser | The import from the server graph |
Directives state the intent, packages enforce it. A forgotten directive produces a runtime error; the package produces a build error, when it costs less.
The number that matters
The EneUI catalogue indexes 111,985 components and 1,556 blocks from 1,472 libraries, measured on 1 November 2026 at `/api/stats`. Many catalogue components read the DOM and want `client-only`: hover cards, animated loaders and form validation.
The case that really breaks is a hook imported by a server component: read a server component importing a client hook. For the `window is not defined` error there is window is not defined in Next.js. For the line between mutations and reads, Route Handlers vs Server Actions. For a framework comparison, Remix vs Next.js.
Known limitation: `server-only` protects the module, not the value. If a server component passes a secret as a prop to a client component, the package says nothing: the datum crosses the boundary inside the HTML response.
Frequently asked questions
What does `server-only` add over `"use server"`?
They are different things. `"use server"` declares an action function; `server-only` stops a server module from ending up in the client bundle.
Does the error appear in development or in the build?
Both, because the bundler resolves the `react-server` condition on every compile. In CI the error blocks the deploy, which is the right moment.
Do I need it in every file that uses the database?
In every file that imports the driver or a secret. A wrapper that re-exports `db` needs the same line, or it reopens the hole.
Is `client-only` needed when the file has `"use client"`?
Yes, when the module truly cannot run on the server. `"use client"` still lets the server import it for prerendering; `client-only` forbids that.
Does it work outside Next.js?
Yes, the packages belong to React and rely on the bundler's `react-server` condition. With Vite or another bundler you need the same condition.