Skip to content

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.

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.

Related questions

Why is Date allowed but Decimal is not?
Date is one of the built-ins React knows how to serialise. A Decimal from a database driver is a class instance with methods, and there is no general way to send behaviour over a network.
Do I have to map every model by hand?
You have to decide what the client needs, which is not the same as mapping everything. In practice the object a component needs is smaller than the row you fetched, and writing that shape down is the useful part.