useParams()

Read the current route's filled-in dynamic segments from a Client Component.

Signature
import { useParams } from 'next/navigation'

const params = useParams()
const params = useParams<{ tag: string; item: string }>()
  • Added in Next.js v13.3.0

Return value

ParamsAn object whose keys are the active dynamic segment names and whose values are a string, or a string[] for catch-all segments. Returns {} on a route with no dynamic params, and null on the initial render in the Pages Router.

Errors

Calling it in a Server Component errors; use the params prop of page.js or layout.js there instead.

Changelog

  • PHP v13.3.0useParams introduced.

useParams is the client counterpart to the params prop that page.js and layout.js receive on the server. Reach for it when a Client Component is nested somewhere the prop cannot reach — a button deep inside a page, a component shared across several dynamic routes, or a layout-level widget that needs the current [id] without prop drilling it through five levels.

Mapping routes to values

RouteURLReturned
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'] }

What trips people up

  • It is not a Promise. The server params prop must be awaited in Next.js 15+; the hook's return value is a plain object you read synchronously. Do not await it.
  • A catch-all segment gives you a string[], so a component that works on [id] will hand you an array on [...id]. Pass a generic to make TypeScript keep you honest.
  • Values are URL-decoded strings, never numbers. Parse before comparing.
  • Route groups such as (marketing) are not params and never appear here.
  • With cacheComponents enabled, the hook suspends on params that are not known at build time, so it needs a Suspense boundary above it.

Example

Example · typescript
'use client'

import { useState } from 'react'
import { useParams } from 'next/navigation'

export default function AddToCart() {
  // Route -> /shop/[tag]/[item]
  // URL   -> /shop/shoes/nike-air-max-97
  // params -> { tag: 'shoes', item: 'nike-air-max-97' }
  const { tag, item } = useParams<{ tag: string; item: string }>()
  const [added, setAdded] = useState(false)

  async function addToCart() {
    await fetch('/api/cart', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ tag, item }),
    })
    setAdded(true)
  }

  return (
    <button type="button" onClick={addToCart} disabled={added}>
      {added ? 'Added' : `Add ${item} to cart`}
    </button>
  )
}

When to use it

  • Read the [id] of the current record inside a delete button nested deep in the page.
  • Share one client form component across /posts/[slug]/edit and /drafts/[slug]/edit.
  • Build an API path such as /api/shop/${tag}/${item} from the current URL.
  • Read the current locale segment in a language switcher rendered in a layout.

Frequently asked questions

What is the difference between useParams and the params prop?

useParams is a Client Component hook that returns the params object synchronously; the params prop is passed to page.js and layout.js on the server and is a Promise you must await in Next.js 15+. Use the prop when you have it and the hook when the component is not a page or layout.

Does useParams return search params too?

No. It returns only dynamic route segments such as [id] or [...slug]. Read the query string with useSearchParams.

Why is my param an array instead of a string?

The segment is a catch-all ([...slug]) or optional catch-all ([[...slug]]), which always produce string[]. A plain [slug] segment produces a string.

More examples

The server equivalent, for comparison

Same two values, two different shapes. On the server params is a Promise and must be awaited; in the Client Component below it, useParams returns the resolved object directly. Use the prop when you are already in a page or layout, and the hook when you are not.

Example · typescript
// app/shop/[tag]/[item]/page.tsx — Server Component, Next.js 15+
export default async function Page({
  params,
}: {
  params: Promise<{ tag: string; item: string }>
}) {
  const { tag, item } = await params
  const product = await getProduct(tag, item)

  return (
    <article>
      <h1>{product.name}</h1>
      <AddToCart />
    </article>
  )
}

Handling a catch-all segment

A catch-all route returns an array rather than a string, so string methods on params.slug throw. The Array.isArray guard keeps the component usable under both [slug] and [...slug], and slicing the array rebuilds the href for each crumb.

Example · typescript
'use client'

import { useParams } from 'next/navigation'

// Route -> app/docs/[...slug]/page.tsx
export default function DocsBreadcrumb() {
  const params = useParams<{ slug: string[] }>()

  // URL -> /docs/app/api-reference/functions
  // params.slug -> ['app', 'api-reference', 'functions']
  const segments = Array.isArray(params.slug) ? params.slug : [params.slug]

  return (
    <ol>
      {segments.map((segment, index) => (
        <li key={segment}>
          <a href={`/docs/${segments.slice(0, index + 1).join('/')}`}>{segment}</a>
        </li>
      ))}
    </ol>
  )
}

Typing params for a shared component

The generic narrows id to string instead of string | string[], so the template literal type-checks. Pairing the hook with useRouter is the usual shape for a destructive action: delete by the id in the URL, leave the now-missing route, then refresh so the list re-renders.

Example · typescript
'use client'

import { useParams, useRouter } from 'next/navigation'

export function DeleteButton() {
  const { id } = useParams<{ id: string }>()
  const router = useRouter()

  async function onDelete() {
    await fetch(`/api/posts/${id}`, { method: 'DELETE' })
    router.replace('/posts')
    router.refresh()
  }

  return (
    <button type="button" onClick={onDelete}>
      Delete post {id}
    </button>
  )
}

Discussion

  • Be the first to comment on this lesson.