page.js

Defines the unique UI for a route segment and makes that URL publicly accessible.

Signature
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

ParameterTypeDescription
$paramsoptionalPromise<{ [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.
$searchParamsoptionalPromise<{ [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.slug is undefined, not a string. You must await params in a Server Component or read it with React's use() in a Client Component.
  • Touching searchParams opts 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 with useSearchParams in a Client Component.
  • searchParams is 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

Example · typescript
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.

Example · typescript
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.

Example · typescript
'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.

Example · typescript
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

  • Be the first to comment on this lesson.