DEV Community

Anas Sheikh
Anas Sheikh

Posted on

Passing a Mongoose Document Straight Into a Client Component Can Fail in a Way That's Genuinely Hard to Diagnose

This is a specific case of a general rule, Server Component to Client Component props need to be serializable, but it's worth calling out on its own because the actual error message you get almost never points clearly at "you passed a Mongoose document," and figuring that out the first time genuinely takes real debugging.

The Setup That Looks Completely Normal

// app/dashboard/page.tsx
import { connectDB } from '@/lib/db';
import User from '@/models/User';
import { UserCard } from '@/components/UserCard';

export default async function DashboardPage() {
  await connectDB();
  const user = await User.findById('some-id'); // note: no .lean() here

  return <UserCard user={user} />;
}
Enter fullscreen mode Exit fullscreen mode
// components/UserCard.tsx
'use client';

export function UserCard({ user }: { user: any }) {
  return <div>{user.name}</div>;
}
Enter fullscreen mode Exit fullscreen mode

UserCard is a Client Component, and user here is a full Mongoose document, not a plain object, since .lean() was never called on the query. This can throw an error during rendering, or in some Next.js versions and configurations, silently serialize into something that mostly works but loses real functionality, depending on exactly what's being accessed and how.

Why This Happens

The boundary between a Server Component and a Client Component is a real serialization boundary, props passed across it get serialized, sent to the client, and reconstructed there. This works cleanly for plain objects, arrays, strings, numbers, booleans. A Mongoose document is not a plain object, it's a class instance with methods, getters, internal Mongoose-specific state, a prototype chain that doesn't survive serialization, since serialization can only meaningfully carry plain data, not behavior or class identity.

Next.js and React are specifically designed to reject or warn about non-serializable values crossing this boundary, exactly to prevent silently broken behavior. Depending on the specific version and exact data shape, you might get a clear-ish error about an object being non-serializable, or, in some cases, get an object that appears to work for simple property access but breaks the moment code tries to call an actual Mongoose method on it, since those methods don't exist anymore on whatever made it across the boundary.

Why the Error Message Rarely Helps

If you do get an error, it typically references React's serialization rules in fairly generic terms, not "this is a Mongoose document specifically." For a developer who hasn't hit this exact issue before, the natural instinct is to suspect the specific prop shape, a nested object, a date field, something concrete and visible, rather than the actual cause, that the entire object is fundamentally the wrong kind of thing to be crossing this boundary at all, regardless of what its individual fields look like.

The Actual Fix: Convert to a Plain Object Before It Crosses the Boundary

// app/dashboard/page.tsx
export default async function DashboardPage() {
  await connectDB();
  const user = await User.findById('some-id').lean(); // now a plain object

  return <UserCard user={user} />;
}
Enter fullscreen mode Exit fullscreen mode

.lean() returns a genuinely plain JavaScript object from the start, no Mongoose document wrapper, no methods, nothing but the actual data, which is exactly what's safe to pass across the Server-to-Client boundary. This is, incidentally, the same optimization covered in an earlier post for performance reasons, faster queries, less overhead, and it turns out to solve this serialization problem too, for the same underlying reason, a lean query never had the non-serializable document behavior to begin with.

When You Can't Just Add .lean()

Sometimes a full Mongoose document is genuinely needed server-side, for its own methods or virtuals, before eventually needing to pass some of that data to a Client Component. In that case, convert explicitly before crossing the boundary, rather than relying on .lean() at the query level:

export default async function DashboardPage() {
  await connectDB();
  const userDoc = await User.findById('some-id'); // full document, methods available here

  const someComputedValue = userDoc.someMethodThatNeedsTheRealDocument();

  const user = userDoc.toObject(); // explicit conversion to a plain object, right before it's needed

  return <UserCard user={user} computedValue={someComputedValue} />;
}
Enter fullscreen mode Exit fullscreen mode

.toObject() (or .toJSON(), which also applies any schema-level transform you've configured, worth being deliberate about which one you actually want) converts a real Mongoose document into a genuinely plain object at the exact point you choose, letting you use the document's real methods first, then hand off something safe to serialize once you're done with it.

The Broader Rule

Anything crossing from a Server Component into a Client Component needs to be a plain, genuinely serializable value, not a class instance, not a Mongoose document, not a function, not a Map or Set without explicit conversion. This applies beyond just Mongoose, any ORM or library returning rich class instances rather than plain data carries the same risk. .lean(), .toObject(), or a manual plain-object mapping are all valid ways to cross that line safely, the specific method matters less than making sure something plain, not something with hidden behavior attached, is what actually gets passed.


If you've hit a confusing serialization error passing data into a Client Component, genuinely curious whether it turned out to be this specific cause, or something else. Drop what actually happened in the comments, this one's error messages are vague enough that I'd guess a lot of people have solved it by trial and error without ever fully understanding why the fix worked.

Get the templates: https://pixelanas.gumroad.com


Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751

Top comments (0)