page.js
Defines the unique UI for a route segment and makes that URL publicly accessible.
export default function Page({
params,
searchParams,
}: {
params: Promise<{ slug: string }>
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}): React.ReactNode- Added in Next.js v13.0.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$paramsoptional | Promise<{ [key: string]: string | string[] }> | Resolves to the dynamic route parameters from the root segment down to this page. Catch-all segments resolve to an array of strings. |
$searchParamsoptional | Promise<{ [key: string]: string | string[] | undefined }> | Resolves to the current URL's query string as a plain object — not a URLSearchParams instance. A repeated key resolves to an array. |
Return value
React.ReactNode | Promise<React.ReactNode>The rendered UI for the route segment. The component may be async, in which case Next.js awaits the returned element.
Errors
Nothing on its own; notFound(), redirect(), forbidden() and unauthorized() called inside a page throw to interrupt rendering of the segment.
Changelog
- PHP v15.0.0-RCparams and searchParams became Promises. Synchronous access still works in 15 for backwards compatibility but is deprecated; a codemod is available.
- PHP v13.0.0page introduced.
A page file is what makes a route segment publicly accessible. A folder without one is just a path fragment — you can nest layout.js, components and helpers in it all day and the URL will still 404. A page is always the leaf of the route subtree.
Reach for it whenever you are adding a new URL. Everything else in the App Router — layouts, loading states, error boundaries — wraps a page; nothing replaces one.
What trips people up
- Both props are Promises since 15.
params.slugisundefined, not a string. You mustawait paramsin a Server Component or read it with React'suse()in a Client Component. - Touching
searchParamsopts the page into dynamic rendering. It is a request-time API, so the page can no longer be fully prerendered. If only part of the page needs the query string, push that read into a child wrapped in<Suspense>. - Layouts do not get
searchParams. Layouts are cached across navigation, so query values would go stale. Read them in the page, or withuseSearchParamsin a Client Component. searchParamsis a plain object..get()does not exist on it.
Pages are Server Components by default; add 'use client' only when the file itself needs hooks or event handlers.
Example
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
return <h1>Blog Post: {slug}</h1>
}When to use it
- Add a new URL such as /pricing or /blog/[slug] to an App Router project
- Fetch and render a record keyed by the dynamic segment in the URL
- Read page, sort and query values off the query string to drive a filtered list
- Render a Client Component page that reads params via React's use() hook
Frequently asked questions
Why is params.slug undefined in my page?
Because params is a Promise as of Next.js 15 — you have to await it: const { slug } = await params. Reading a property straight off the prop gives undefined. In a Client Component, unwrap it with React's use(params) instead, since the component cannot be async.
Why does my page URL return a 404 when the folder exists?
A route segment is only publicly accessible once it contains a page file. Folders holding just a layout.js, components or utilities do not produce a URL. Add page.tsx to the deepest folder you want reachable — a page is always the leaf of the route subtree.
Can I use searchParams in a layout?
No. Layouts do not re-render on navigation, so query params would go stale and Next.js does not pass them. Read searchParams in the page and pass the values down, or use the useSearchParams hook inside a Client Component, which does re-render on every navigation.
More examples
Filtering a list with searchParams
For the URL /shop?query=desk&page=2 this renders "Search query: desk" and "Current page: 2". The defaults in the destructuring cover the bare /shop case. Because searchParams was awaited at the top level, this page is rendered at request time on every visit rather than prerendered.
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) {
const { page = '1', sort = 'asc', query = '' } = await searchParams
return (
<div>
<h1>Product Listing</h1>
<p>Search query: {query}</p>
<p>Current page: {page}</p>
<p>Sort order: {sort}</p>
</div>
)
}Reading both props in a Client Component
A Client Component cannot be async, so the Promises are unwrapped with React's use() hook instead of await. The component suspends until both resolve, which is why a loading.js or Suspense boundary above it is worth having.
'use client'
import { use } from 'react'
export default function Page({
params,
searchParams,
}: {
params: Promise<{ slug: string }>
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) {
const { slug } = use(params)
const { query } = use(searchParams)
return (
<p>
{slug} / {query}
</p>
)
}Typing params from the route literal
PageProps is a global helper generated during next dev, next build or next typegen — it needs no import. Passing the route literal '/blog/[slug]' gives slug autocomplete and rejects keys that are not in the path, so renaming a folder surfaces as a type error instead of an undefined at runtime.
export default async function Page(props: PageProps<'/blog/[slug]'>) {
const { slug } = await props.params
const query = await props.searchParams
return <h1>Blog Post: {slug}</h1>
}
Discussion