useParams()
Read the current route's filled-in dynamic segments from a Client Component.
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
| Route | URL | Returned |
|---|---|---|
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
paramsprop must be awaited in Next.js 15+; the hook's return value is a plain object you read synchronously. Do notawaitit. - 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
cacheComponentsenabled, the hook suspends on params that are not known at build time, so it needs aSuspenseboundary above it.
Example
'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.
// 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.
'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.
'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