Fix Next.js Hydration Errors Caused by Client-Only State (Cart Badge Case)
Next.js Hydration React State Client State
Hydration errors in Next.js are annoying, noisy, and always a signal of a real bug—not something you should silence.
One of the most common cases is a cart badge that depends on client-side state.
Let’s break it down and fix it properly.
The Problem: Server and Client Render Different UI
You have code like this:
{totalItems > 0 && (
<Badge variant="destructive">{totalItems}</Badge>
)}
At first glance, this looks fine. It’s not.
What Actually Happens
-
Server render (SSR)
-
totalItemsis0orundefined -
No
<Badge />is rendered
-
-
Client hydration
-
totalItemsbecomes3(from Zustand, Redux,localStorage, etc.) -
<Badge>3</Badge>suddenly appears
-
💥 Boom — hydration mismatch
React sees:
-
Server HTML: no badge
-
Client HTML: badge exists
React throws a hydration error because the DOM doesn’t match.
Root Cause (Be Honest About It)
The root cause is simple:
You are rendering client-only data during SSR
Common sources:
-
Zustand / Redux store hydrated from
localStorage -
Context initialized in
useEffect -
Browser-only APIs
-
Client-only API calls
🚫 The server cannot access any of these.
The Correct Fix (Recommended)
✅ Option 1: Use a Mounted Guard (Best Practice)
Only render client-dependent UI after the component mounts.
"use client";
import { useEffect, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { ShoppingCart } from "lucide-react";
export function CartIcon({ totalItems }: { totalItems: number }) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return (
<div className="relative">
<ShoppingCart className="h-6 w-6" />
{mounted && totalItems > 0 && (
<Badge
variant="destructive"
className="absolute -top-2 -right-2 h-5 w-5 flex items-center justify-center p-0 text-xs"
>
{totalItems}
</Badge>
)}
</div>
);
}
Why This Works
| Phase | What Renders |
|---|---|
| Server | Cart icon only |
| Client (first render) | Cart icon only |
| After mount | Cart icon + badge |
✅ Server and client HTML match
✅ No hydration mismatch
✅ Clean mental model
This pattern is mandatory when using client-only state.
Alternative (Sometimes Useful, Not Enough Alone)
✅ Option 2: Initialize State Consistently
If you’re using Zustand or Redux, never let values be undefined.
// store/cartStore.ts
const useCartStore = create(() => ({
totalItems: 0, // always predictable
}));
⚠️ Important:
This does NOT fully solve the problem if:
-
Real data comes from
localStorage -
Data is fetched only on the client
You’ll still need the mounted guard.
What NOT To Do (Seriously, Don’t)
🚫 suppressHydrationWarning
<div suppressHydrationWarning>
{totalItems > 0 && <Badge>{totalItems}</Badge>}
</div>
This:
-
Hides the error
-
Keeps the bug
-
Makes future bugs harder to detect
Use this only as a last resort for legacy code.
Bonus: Clean Up Your JSX
This class is repeated and noisy:
className="absolute -top-2 -right-2 h-5 w-5 flex items-center justify-center p-0 text-xs"
Move it into:
-
A reusable component
-
A variant
-
Or a utility class
Cleaner JSX = fewer bugs.
Mental Rule to Remember
If data comes from the browser, don’t render it on the server.
Use:
-
mountedguard -
dynamic(() => import(...), { ssr: false }) -
Or server-provided data
But never mix SSR markup with client-only values.
Final Verdict
-
✅ Hydration errors are real bugs
-
✅ Mounted guards are the safest fix
-
🚫 Don’t silence warnings
-
🚫 Don’t trust client state during SSR
If your totalItems comes from Zustand, Redux, or Context, say the word—I’ll give you a store-level hydration pattern that scales cleanly.