Skip to content
C.W.K.
Stream
Lesson 02 of 10 · published

page.tsx — The Route Component

~22 min · page, params, Promise

Level 0Curious
0 XP0/68 lessons0/11 achievements
0/120 XP to next level120 XP to go0% complete

The shape of a page

A page.tsx exports a default React component. By default it's a Server Component, so it can be async. Server Components don't take many props — the framework injects route data via two specific props:

  • params: route parameters from [slug]-style segments.
  • searchParams: query string parameters from the URL.

Both props are Promises in v15+

The big breaking change from v14 to v15: params and searchParams are Promises. You must await them. In Client Components, use React.use(params). The Promise wrap unblocks parallel rendering — nothing has to wait for params resolution before it starts.

Type helpers

Next.js publishes typed helpers under the next module entry. PageProps<'/blog/[slug]'> gives you a fully typed params based on the literal route path.

Treat params and searchParams as untrusted request input, not as convenient typed state. Await them, validate the values against the domain, and end missing-resource paths with notFound(). PageProps protects the names implied by the route literal; it does not prove that a slug exists or that a query value is allowed.

The Promise API is not ceremony for its own sake. It lets unrelated rendering work begin before route values have resolved. The wrong response is to hide the Promise behind an unsafe cast. Keep the asynchronous boundary visible where the framework hands the request to your page. Exercise the page with a valid path, a missing segment, repeated query keys, and a value outside the allowed set. Then rename [slug] to [postId] and confirm the generated type follows the folder. A passing happy path alone does not test the route contract.

Code

Async page with awaited params·tsx
// app/blog/[slug]/page.tsx
type Props = {
  params: Promise<{ slug: string }>;
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};

export default async function BlogPost({ params, searchParams }: Props) {
  const { slug } = await params;
  const { sort = 'newest' } = await searchParams;
  const post = await fetchPost(slug, { sort });

  return (
    <article>
      <h1>{post.title}</h1>
      <p className="text-sm text-gray-500">sort: {sort}</p>
      <div dangerouslySetInnerHTML={{ __html: post.html }} />
    </article>
  );
}
Typed via PageProps·tsx
import type { PageProps } from 'next';

export default async function Page({ params }: PageProps<'/blog/[slug]'>) {
  const { slug } = await params; // typed: { slug: string }
  return <Article slug={slug} />;
}
Client Component variant·tsx
'use client';
import { use } from 'react';

export default function ClientPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = use(params); // React.use unwraps the Promise
  return <div>Slug: {slug}</div>;
}

External links

Exercise

Pick one of your existing pages with params and searchParams. Convert it to await both, type it with PageProps, and verify nothing regresses.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.