You know when you drop useParams()
into a client component in Next.js… but the server parent still throws an error?
Error: useParams only works in a Client Component.
Even though you added "use client"
, Next.js refuses to play nice.
I hit this exact wall — and here’s what’s actually happening, plus the fixes that worked.
Why This Happens
In the App Router, components are split into two worlds:
-
Server Components → render on the server, no browser-only hooks allowed (
useParams
,usePathname
, etc.). - Client Components → run in the browser, can use those hooks.
The problem: if a server component depends on a client one that uses useParams
, the server still tries to evaluate early. Without the right setup, you get hydration mismatches or runtime errors.
Fix #1 — Wrap in <Suspense>
The easiest fix is to tell the server: “hey, this client bit might suspend, wait for it.”
// app/[slug]/page.tsx
import { Suspense } from "react";
import SlugClient from "./SlugClient";
export default function Page() {
return (
<Suspense fallback={<div>Loading…</div>}>
<SlugClient />
</Suspense>
);
}
// app/[slug]/SlugClient.tsx
"use client";
import { useParams } from "next/navigation";
export default function SlugClient() {
const { slug } = useParams();
return <div>Slug is: {slug}</div>;
}
Now the parent server component won’t choke — it streams the client side once it’s ready.
Fix #2 — Force Dynamic Rendering
Sometimes you want runtime rendering instead of static. That’s when you tell Next.js explicitly:
// app/[slug]/page.tsx
export const dynamic = "force-dynamic";
This skips static pre-rendering and makes sure params resolve at runtime.
Fix #3 — Pass Params From Server → Client
If you don’t actually need useParams
inside the client, just forward params
down:
// app/[slug]/page.tsx
export default function Page({ params }: { params: { slug: string } }) {
return <SlugClient slug={params.slug} />;
}
// app/[slug]/SlugClient.tsx
"use client";
export default function SlugClient({ slug }: { slug: string }) {
return <div>Slug is: {slug}</div>;
}
Cleaner, safer, and avoids the hook altogether.
Which One Should You Use?
- ✅ Want SEO + predictable params? →
generateStaticParams
or pass them down. - ⚡ Need runtime flexibility? →
force-dynamic
. - 🎯 Stuck with client-only hooks (
useParams
,usePathname
)? → wrap in<Suspense>
.
I turned my little “why is this broken?” moment into a fix that saved me hours. Hopefully, it saves you too.
👉 Have you hit this bug before? How did you solve it?
Let’s continue the conversation on X (Twitter).
Top comments (0)