Type something to search...
Next.js useParams

Next.js useParams

Every dynamic route in the App Router hands its filled-in segments to the matching page.js and layout.js files as a params prop. That works fine when the component that needs the value is the page or layout itself. It works a lot less fine when the component that needs it is three levels deep in a Client Component tree — a breadcrumb, a tab indicator, an analytics call — and prop-drilling params down through every intermediate component just to reach it would mean touching files that have nothing to do with routing.

useParams exists for exactly that gap. It's a Client Component hook, imported from next/navigation, that lets any client-rendered component read the current route's dynamic segments directly, without needing them passed in as a prop from anywhere.

What useParams actually gives you

Here's the shape of it:

"use client";

import { useParams } from "next/navigation";

export default function ExampleClientComponent() {
  const params = useParams<{ tag: string; item: string }>();

  // Route -> /shop/[tag]/[item]
  // URL   -> /shop/shoes/nike-air-max-97
  // params -> { tag: 'shoes', item: 'nike-air-max-97' }

  return (
    <p>
      {params.tag} / {params.item}
    </p>
  );
}

It takes no arguments. It returns a plain object where each key is the name of an active dynamic segment in the current route, and each value is either a string or a string[], depending on what kind of segment it is:

RouteURLuseParams()
app/shop/page.js/shop{}
app/shop/[slug]/page.js/shop/1{ slug: '1' }
app/shop/[tag]/[item]/page.js/shop/1/2{ tag: '1', item: '2' }
app/shop/[...slug]/page.js/shop/1/2{ slug: ['1', '2'] }

Notice that a route with no dynamic segments at all just returns an empty object rather than null or undefined — you can destructure it safely without an existence check, though a specific key you expect might still be missing if the component is rendered somewhere the route doesn't actually pass through.

The generic type parameter in the example (useParams<{ tag: string; item: string }>()) is worth calling out on its own. useParams can't infer the shape of your params from the route file at the point where you call it — TypeScript has no way to trace "this component is rendered inside app/shop/[tag]/[item]/page.js" back to the hook call. So without the generic, params comes back typed as a loose Record<string, string | string[]>, and you lose autocomplete and type safety on every property access. Passing the generic explicitly is a small bit of manual bookkeeping, but it's the only way to get params typed the way you'd actually want them typed.

Why this hook exists at all

If you're coming from Server Components, the natural question is: why not just use the params prop everywhere? The answer is the client/server boundary. page.js and layout.js files receive params because Next.js controls their invocation directly — it can inject a prop into a function it's calling itself. But once you cross into 'use client' territory, you're in ordinary React composition rules. A Client Component only receives what its parent explicitly passes it, and if that Client Component is buried under three other Client Components that don't care about params, you'd have to add a params prop to every single one of them just to relay a value none of them use.

useParams sidesteps that by reading the current route state from React Context under the hood, set up by the Next.js router itself. Any Client Component, anywhere in the tree, can call it and get the current segments without a single prop being threaded through.

This is also the reason useParams is Client Component-only. Route params are already available as a prop in Server Components, and there's no router context to read from during server rendering in the same sense — the value is simply passed down as an argument instead.

Cache Components and the Suspense boundary you might need

If your project has the cacheComponents flag enabled in next.config.js, useParams behaves differently depending on how predictable your dynamic segments are:

Static routes, and routes using generateStaticParams — every value the segment can take is known ahead of time, so useParams resolves during prerendering with no extra work from you.

Routes with dynamic params not covered by generateStaticParams — the actual value isn't known until a real request comes in, so useParams suspends. If nothing above it in the tree is wrapped in a <Suspense> boundary, the build fails outright rather than silently working around it.

import { Suspense } from "react";
import ExampleClientComponent from "./example-client-component";

export default function Page() {
  return (
    <Suspense fallback={<p>Loading…</p>}>
      <ExampleClientComponent />
    </Suspense>
  );
}

This trips people up because it's a build-time failure for what looks like a purely client-side hook — there's no visual cue in the component itself that it needs a boundary above it. If you hit this, the fix is either wrapping the component in Suspense as above, adding the missing case to generateStaticParams, or accepting that this piece of UI genuinely can't be part of the static shell and structuring around that. Next.js's own error message for this situation ("Next.js encountered URL data in a Client Component outside of Suspense") points at the same trade-offs.

The Pages Router difference

If you're maintaining a project that still has Pages Router routes (or you're mid-migration and have both routers active), useParams behaves slightly differently there: on the very first render it returns null, and only starts returning the actual params object once the router has finished initializing on the client. In the App Router, you don't get this null state — by the time your component runs, the params are already resolved. If you're sharing a component between both routers, that null case is one more thing you have to guard against explicitly that App-Router-only code doesn't need to.

Practical patterns

Breadcrumbs built from the URL shape. Since useParams gives you the raw segment values without needing to parse usePathname() yourself, it's a clean source for breadcrumb trails:

"use client";

import { useParams } from "next/navigation";
import Link from "next/link";

export function CategoryBreadcrumb() {
  const { category, product } = useParams<{
    category: string;
    product?: string;
  }>();

  return (
    <nav aria-label="Breadcrumb">
      <Link href="/shop">Shop</Link>
      {category && (
        <>
          {" "}
          / <Link href={`/shop/${category}`}>{category}</Link>
        </>
      )}
      {product && (
        <>
          {" "}
          / <span>{product}</span>
        </>
      )}
    </nav>
  );
}

Tagging analytics events with route context. A shared analytics wrapper deep in the tree can attach the current dynamic segment to every event it fires, without every page needing to remember to pass it in:

"use client";

import { useParams } from "next/navigation";

export function trackEvent(name: string, extra: Record<string, unknown> = {}) {
  // called from inside a component using useParams()
}

export function useTrackedEvent() {
  const params = useParams();
  return (name: string, extra: Record<string, unknown> = {}) =>
    trackEvent(name, { ...extra, routeParams: params });
}

Highlighting the active item in a list that doesn't otherwise know the route. A sidebar or tab bar rendered as a Client Component (for its own interactivity reasons) can compare its own items against useParams() to decide what's currently selected, instead of needing that state passed down from a parent that owns the routing logic.

Common mistakes

Reaching for useParams when you actually want useSearchParams. These solve different problems and the names are easy to conflate under pressure. useParams reads dynamic route segments — the [slug] part of the path. useSearchParams reads the query string — the ?sort=price part. If your value shows up after a ? in the URL, you want the other hook.

Calling it from a Server Component. useParams is explicitly a Client Component hook. In a Server Component, you already have the same information for free as the params prop — reach for that instead, and only cross into useParams territory once you're actually inside a 'use client' boundary that has no other way to get the value.

Assuming a key exists just because the route "usually" has it. If a shared Client Component is reused across multiple routes — some with a [tag] segment, some without — destructuring const { tag } = useParams() will silently give you undefined on the routes that don't have it, rather than throwing. Treat every property as optional unless the component is only ever rendered under one specific route shape.

Forgetting the Suspense requirement is conditional, not automatic. It only bites you on dynamic segments outside generateStaticParams under Cache Components — plenty of projects will never see it. But when you do hit it, it shows up as a build failure with no visual hint in the component code itself, so it's worth knowing the shape of the error in advance rather than discovering it cold.

Key Takeaways

SituationWhat to reach for
Reading dynamic segments in a Server ComponentThe params prop
Reading dynamic segments deep in a Client Component treeuseParams
Reading query string values (?key=value)useSearchParams
Reading the current URL path itselfusePathname
Dynamic segment not covered by generateStaticParams, under Cache ComponentsWrap the component in Suspense
Sharing a component between Pages Router and App RouterGuard for the null initial-render case Pages Router returns

useParams is a small, single-purpose hook, but it closes a real gap: without it, any Client Component that needs to know "what route am I actually rendered under" would have no way to find out except by having that information manually passed down from above. It's the client-side mirror of the params prop, and reaching for the right one of the two depends entirely on which side of the 'use client' boundary the component asking the question lives on.

Tags :
Share :

Related Posts

Can Next.js Be Used with GraphQL?

Can Next.js Be Used with GraphQL?

Next.js and GraphQL are two powerful technologies that have gained significant traction in the web development community. Next.js, a React-based fram

Dive Deeper
How does Next.js differ from Create React App?

How does Next.js differ from Create React App?

In the world of modern web development, React.js has emerged as a dominant force due to its flexibility, performance, and extensive ecosystem. Two po

Dive Deeper
How does Next.js handle image optimization?

How does Next.js handle image optimization?

In modern web development, image optimization plays a critical role in enhancing user experience and improving site performance. Large, unoptimized i

Dive Deeper