Converting mongo _id to serialisable object with nextjs server/client components

211 Views Asked by At

I'm using nextjs with app router and server/client components and a common pattern I have is retrieving data from mongo in the page.js which as a rule I'm trying to keep as server components.

After getting the data I need to pass it on to 1-N client components. If I just return the mongo document I get the following nextjs warning:

Warning: Only plain objects can be passed to Client Components from Server Components. Objects with toJSON methods are not supported. Convert it manually to a simple value before passing it to props.   {_id: {}, userId: ..., userMode: ... }

This seems to point to the mongo _id property which is an object.

At the moment I'm having to remap the _id property in the server component after getting the data which feels really clunky. e.g.

async function getData() {
  const { user } = await getSession();

  const { db } = await connectToDatabase();

  const strategy = await db.collection("pricing").findOne({ userId: user.sub });

  const jsonStrategy = { ...strategy, id: strategy._id.toString() };

  return { strategy: jsonStrategy };
}

It feels like I'm missing something here, is there a better way to pass on the mongo data without needing to map the id from every object on every page?

2

There are 2 best solutions below

1
On

I also was stuck with this problem but found a workaround but I don't know if it is the best practice.
Whenever I pass an mongodb id I stringify it when passing it to a client component

<ClientComponent
    _id={JSON.stringify(userInfo?._id)}
/>

But keep in mind you have to parse the value before using it again

 likePost({
     threadId: JSON.parse(id),
     userId: JSON.parse(currentUser_id),
   });

Please let me know if you find something better.

0
On

There's a couple approaches you could use here as alternatives:

  • Alter the return from mongoose to avoid Document properties altogether using something like findOne().lean() to get just plain objects from the start.
  • More helpfully, there's a built in Document.toObject which can accept options { flattenObjectIds: true }. That should get you something a little easier to work with.

I use this function to change an entire set of documents into objects before passing to client components:

import { Document } from "mongoose";

/**
 * Reshapes a set of document to it's object version.
 * Typically used to convert a MongoDB Document from the server side to "use client" components.
 */
export const toObjects = <T extends Document>(
  documents: T[],
): Omit<T, keyof Document>[] =>
  documents.map((document) => document.toObject({ flattenObjectIds: true }));