Search code examples
javascripttypescriptmongodbmongoosenext.js

Next.js 14: Hydration Error | Warning: Only plain objects can be passed to Client Components from Server Components


I'm developing an app with Next.js 14, TypeScript, Mongoose & MongoDB.

I fetched users from my database and rendered them in cards with some of their information, like tags displayed in badges.

I also used the Link component to make the cards and the badges clickable.

Here is some of my relevant code:

  • "components\cards\UserCard.tsx":
import Link from "next/link";
import Image from "next/image";

import { getTopInteractedTags } from "@/lib/actions/tag.action";
import { Badge } from "../ui/badge";
import RenderTag from "../shared/RenderTag";

interface Props {
  user: {
    _id: string;
    clerkId: string;
    picture: string;
    name: string;
    username: string;
    // orgId?: string;
  };
}

const UserCard = async ({ user }: Props) => {
  const interactedTags = await getTopInteractedTags({ userId: user._id });

  return (
    <Link
      href={`/profile/${user.clerkId}`}
      className="shadow-light100_darknone w-full max-xs:min-w-full xs:w-[260px]"
    >
      <article className="background-light900_dark200 light-border flex w-full flex-col items-center justify-center rounded-2xl border p-8">
        <Image
          src={user.picture}
          alt="image profil utilisateur"
          width={100}
          height={100}
          className="rounded-full"
        />

        <div className="mt-4 text-center">
          <h3 className="h3-bold text-dark200_light900 line-clamp-1">
            {user.name}
          </h3>
          <p className="body-regular text-dark500_light500 mt-2">
            @{user.username}
          </p>
          {/* {user.orgId && (
            <p className="body-regular text-dark500_light500 mt-2">
              Org ID: {user.orgId}
            </p>
          )} */}
        </div>

        <div className="mt-5">
          {interactedTags.length > 0 ? (
            <div className="flex items-center gap-2">
              {interactedTags.map((tag) => (
                <RenderTag key={tag._id} _id={tag._id} name={tag.name} />
              ))}
            </div>
          ) : (
            <Badge>Pas encore d’étiquettes</Badge>
          )}
        </div>
      </article>
    </Link>
  );
};

export default UserCard;
  • "components\shared\RenderTag.tsx":
import React from "react";
import Link from "next/link";

import { Badge } from "@/components/ui/badge";

interface Props {
  _id: string;
  name: string;
  totalQuestions?: number;
  showCount?: boolean;
}

const RenderTag = ({ _id, name, totalQuestions, showCount }: Props) => {
  return (
    <Link href={`/etiquettes/${_id}`} className="flex justify-between gap-2">
      <Badge className="subtle-medium background-light800_dark300 text-light400_light500 rounded-md border-none px-4 py-2 uppercase">
        {name}
      </Badge>

      {showCount && (
        <p className="small-medium text-dark500_light700">{totalQuestions}</p>
      )}
    </Link>
  );
};

export default RenderTag;

However, I get this error:

Error: Hydration failed because the initial UI does not match what was rendered on the server. See more info here: https://nextjs.org/docs/messages/react-hydration-error

Expected server HTML to contain a matching in .

I tried many solutions, like replacing the article with a div or passing the suppressHydrationWarning property to the related elements to remove this warning. However, these solutions didn't help me fix this issue.

Therefore, I came out with another alternative which is using useRouter instead of Link for the navigation.

Here is my code:

  • "components\shared\RenderTag.tsx":
"use client";

import { useRouter } from "next/navigation";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
interface Props {
  _id: string;
  name: string;
  totalQuestions?: number;
  showCount?: boolean;
}

const RenderTag = ({ _id, name, totalQuestions, showCount }: Props) => {
  const router = useRouter();
  return (
    <Button
      className="flex justify-between gap-2"
      onClick={() => router.push(`/tags/${_id}`)}
    >
      <Badge className="subtle-medium background-light800_dark300 text-light400_light500 rounded-md border-none px-4 py-2 uppercase">
        {name}
      </Badge>
      {showCount && (
        <p className="small-medium text-dark500_light700">{totalQuestions}</p>
      )}
    </Button>
  );
};

export default RenderTag;

Now, the hydration error disappears, but I get this warning in my terminal:

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={{buffer: ...}} name="next.js">

Where this error is coming from, especially this part <... _id={{buffer: ...}} name="next.js">?

I guess it is related to my database "tags" collection whis has the following data:

{"_id":{"$oid":"66a476d397749b79a8140e72"},"__v":{"$numberInt":"0"},"createdOn":{"$date":{"$numberLong":"1722054355742"}},"followers":[],"name":"next.js","questions":[]}
{"_id":{"$oid":"66a5164197749b79a8a3258b"},"__v":{"$numberInt":"0"},"createdOn":{"$date":{"$numberLong":"1722095169919"}},"followers":[],"name":"React","questions":[]}

But, why I get this error only when I use useRouter?


Solution

  • Let's take a look at your first code: Your RenderTag component is within a Link component and contains a Link component itself (Link wrapped inside another Link), now imagine trying to navigate to /etiquettes/${_id} and you end up in /profile/${user.clerkId}, that would be a problem, this is what's causing the hydration error, if you want to fix this problem, you can remove the global Link component in the UserCard and wrap each of the divs, Images, and other components in a Link component, but not the RenderTag, this way you can ensure the hydration error does not happen. Example:

    /* imports and type declarations */
    const UserCard = async ({ user }: Props) => {
      const interactedTags = await getTopInteractedTags({ userId: user._id });
    
      return (
        <div
          className="shadow-light100_darknone w-full max-xs:min-w-full xs:w-[260px]"
        >
          <article className="background-light900_dark200 light-border flex w-full flex-col items-center justify-center rounded-2xl border p-8">
        <Link href={`/profile/${user.clerkId}`}>
            <Image
              src={user.picture}
              alt="image profil utilisateur"
              width={100}
              height={100}
              className="rounded-full"
            />
        </Link> {/* do the same for other divs except the one containing RenderTag*/}
    
            <div className="mt-4 text-center">
              <h3 className="h3-bold text-dark200_light900 line-clamp-1">
                {user.name}
              </h3>
              <p className="body-regular text-dark500_light500 mt-2">
                @{user.username}
              </p>
              {/* {user.orgId && (
                <p className="body-regular text-dark500_light500 mt-2">
                  Org ID: {user.orgId}
                </p>
              )} */}
            </div>
    
            <div className="mt-5"> {/* should not be wrapped in a <Link/> */}
              {interactedTags.length > 0 ? (
                <div className="flex items-center gap-2">
                  {interactedTags.map((tag) => (
                    <RenderTag key={tag._id} _id={tag._id} name={tag.name} />
                  ))}
                </div>
              ) : (
                <Badge>Pas encore d’étiquettes</Badge>
              )}
            </div>
          </article>
        </div>
      );
    };
    
    export default UserCard;