DEV Community

Daniel | Frontend developer
Daniel | Frontend developer

Posted on

When useParams Breaks Your Next.js App (And How I Fixed It)

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>
  );
}
Enter fullscreen mode Exit fullscreen mode
// app/[slug]/SlugClient.tsx
"use client";

import { useParams } from "next/navigation";

export default function SlugClient() {
  const { slug } = useParams();
  return <div>Slug is: {slug}</div>;
}
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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} />;
}
Enter fullscreen mode Exit fullscreen mode
// app/[slug]/SlugClient.tsx
"use client";

export default function SlugClient({ slug }: { slug: string }) {
  return <div>Slug is: {slug}</div>;
}
Enter fullscreen mode Exit fullscreen mode

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)