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:
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;
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:
"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
?
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 div
s, Image
s, 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;