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

ToolWhat it declaresWhat it blocks
`"use client"`A client module and its subtreeNothing: the server may still import it
`"use server"`An action functionNothing: the client receives a reference
`server-only`A module that lives on the serverThe import from the client graph
`client-only`A module that lives in the browserThe 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.