Next.js error
Only Plain Objects Can Be Passed to Client Components
The error
Error: Only plain objects, and a few built-ins, can be passed to Client Components from Server Components. Classes or null prototypes are not supported.Something crossed the server-client boundary that cannot be serialised - usually a database model, a Decimal, or an object with methods on it.
A Server Component's output is serialised and sent to the browser. Strings,
numbers, booleans, arrays, plain objects, Date, Map, Set and Server
Action references survive that. Class instances do not, because their methods
and prototype cannot travel.
The error usually means a database model reached a Client Component untouched.
The three usual culprits
A Decimal or BigInt from the database driver. Prisma's Decimal, a
driver's BigInt, a money type - all class instances. Convert at the
boundary: price.toNumber(), or better, format to the string you intend to
display.
A full model object. <ProductCard product={product} /> where product
came straight from the ORM. Even when every field looks plain, drivers often
attach non-enumerable properties or a null prototype.
Something with a method on it. A domain object with isExpired() or
format(). The client needs the answer, not the function - compute it on the
server and send the result.
The fix
Map to the shape the component actually needs, on the server:
// app/products/page.tsx - Server Component
const rows = await db.product.findMany();
const products = rows.map((p) => ({
id: p.id,
name: p.name,
price: p.price.toNumber(), // Decimal -> number
releasedAt: p.releasedAt, // Date is fine
isNew: Date.now() - p.releasedAt.getTime() < WEEK, // answer, not method
}));
return <ProductGrid products={products} />;This looks like boilerplate and is not. It is the only place in the codebase where you state what the browser is allowed to know, which means it is also where you stop leaking the columns you did not mean to send - password hashes, internal flags, other people's identifiers. A component that receives the whole row receives all of it, and it is all in the HTML.
What looks like a fix and is not
JSON.parse(JSON.stringify(data)). It works, and it is the reason this
error is usually resolved without being understood. Every Date becomes a
string, every undefined disappears, NaN becomes null, and a Decimal
silently loses precision. You have swapped a build error for data corruption
that shows up as a formatting bug three screens away.
Adding 'use client' higher up. The boundary moves, the error goes, and
now your database call is in the client bundle - which either fails to build
or, if the import happens to be isomorphic, ships credentials.
Where use client sits is the decision you just
made by accident.
Casting to any. TypeScript stops complaining. React still cannot
serialise it.
The related error
If what crossed was a function rather than an object, the message is different and so is the fix: event handlers cannot be passed to Client Component props.
