Type something to search...
Next.js Layouts and Pages

Next.js Layouts and Pages

Every React project eventually asks the same question: how do URLs map to components? In the Pages Router, the answer was a flat mapping between a file in pages/ and a route. The App Router replaces that with something more powerful and, at first glance, more confusing: a folder structure where the folders define the URL and specially named files inside those folders define what actually renders. Once the model clicks, it removes an entire category of routing boilerplate you'd otherwise hand-roll yourself — nested layouts, shared UI across sibling routes, colocated data fetching per segment.

This article walks through the two files that do almost all of the work — page.tsx and layout.tsx — and how folder nesting, dynamic segments, and the <Link> component combine with them to build a real, multi-page application. I'll also cover the parts the official docs mention only in passing: what actually breaks when you get the root layout wrong, why searchParams quietly opts a route into dynamic rendering, and the newly-typed PageProps/LayoutProps helpers that remove a whole class of "I passed the wrong shape of params" bugs.

File-System Routing, in Practice

Next.js uses file-system based routing: every folder inside app/ is a route segment, and the URL is built by walking the folder path from app/ down. app/blog/page.tsx becomes /blog. app/blog/[slug]/page.tsx becomes /blog/<anything>. There's no route config file to maintain, no array of { path, component } objects to keep in sync with your file tree — the tree is the config.

But folders alone don't render anything. A folder just defines a URL segment; it takes a specific file inside that folder to actually produce output for that segment. The two files you'll use constantly are:

  • page.tsx — makes a route segment publicly accessible and renders the UI for that specific URL.
  • layout.tsx — renders UI that's shared across a segment and everything nested beneath it.

This separation is the single most important idea to internalize before anything else here makes sense: a folder is a URL segment, and files inside it decide whether that segment is reachable (page) and what wraps it (layout). Get comfortable with that distinction and the rest of App Router routing — parallel routes, intercepting routes, route groups — all reads as variations on this same theme.

Creating a Page

A page is UI rendered for one specific route. To create one, add a page file inside a folder in app/ and default-export a React component. To create the index page at /:

// app/page.tsx
export default function Page() {
  return <h1>Hello Next.js!</h1>;
}

That's the entire contract. No route registration, no getStaticProps boilerplate required just to render something. The function name doesn't matter to Next.js — it's convention to call every page component Page, since you'll have dozens of files with that identical export name scattered across your project, and nobody outside that file will ever import it directly.

What does matter is the filename (page.tsx, page.jsx, or page.js) and its location. Only files literally named page make a segment routable. This is deliberate: it means you can put components, utilities, tests, and styles inside the same folder as a route without accidentally exposing them as pages. A app/blog/utils.ts file sits right next to app/blog/page.tsx and is never treated as a route, because it isn't named page.

Creating a Layout

A layout is UI shared between multiple pages. This is the part that genuinely wasn't clean to do in the Pages Router without a custom _app.tsx hack or a manual wrapper component repeated on every page. Layouts solve it natively, and they come with a property that's easy to skim past but is actually a big deal: on navigation, layouts preserve state, remain interactive, and do not re-render.

Read that again, because it's not the same guarantee React gives you by default. If you click between two blog posts that share a layout containing, say, an open <details> element, a scroll position, or a controlled form input, that state survives the navigation. The layout component itself isn't remounted — only the children slot underneath it swaps out. This is why sidebars, navigation bars, and persistent audio players belong in layouts rather than being duplicated per-page: you get the "shared shell" behavior for free, without wiring up your own persistence layer.

You define a layout by default-exporting a component that accepts a children prop:

// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <main>{children}</main>
      </body>
    </html>
  );
}

This particular layout, sitting directly at the root of app/, is called the root layout, and it's not optional. Every App Router project must have one, and it's the only layout in your tree that's required to render <html> and <body> tags — nested layouts further down the tree should not repeat them. If you forget the root layout entirely, next dev will refuse to build your app. If you add <html>/<body> tags to a nested layout instead of the root one, you'll get duplicate document elements and very confusing hydration warnings — I've lost time to this exact mistake more than once, usually after copy-pasting a layout file from a different nesting depth than I meant to.

One more practical note the docs don't dwell on: because the root layout wraps literally everything, anything you put there — a font loader, a global CSS import, an analytics script, a client-side context provider — runs on every single route in your app, with no way to opt a specific page out. If you need a page that behaves completely differently (a print-friendly invoice page, an embed with no chrome), you're better off giving it a route group with its own layout rather than fighting the root layout with conditional rendering.

Creating a Nested Route

A nested route is just a route made of more than one URL segment. /blog/[slug] breaks down into three segments:

  • / — the root segment
  • blog — a segment
  • [slug] — a leaf segment (and, as the brackets suggest, a dynamic one — more on that shortly)

To build this, you nest folders inside app/ the same way the URL nests. Want /blog to exist? Create app/blog/ and drop a page.tsx in it:

// app/blog/page.tsx
import { getPosts } from "@/lib/posts";
import { Post } from "@/ui/post";

export default async function Page() {
  const posts = await getPosts();

  return (
    <ul>
      {posts.map((post) => (
        <Post key={post.id} post={post} />
      ))}
    </ul>
  );
}

Notice this page component is async. That's not special App Router syntax bolted onto the language — page and layout components in the App Router are ordinary React Server Components by default, and Server Components are allowed to be async functions that await data directly in the component body. There's no getServerSideProps or getStaticProps export to remember; you just fetch inside the component. This single change removes an entire category of "which data-fetching function do I need for this page" decision fatigue that Pages Router developers used to deal with constantly.

To go one level deeper and add a route for an individual blog post, nest another folder inside blog/:

// app/blog/[slug]/page.tsx
export function generateStaticParams() {}

export default function Page() {
  return <h1>Hello, Blog Post Page!</h1>;
}

Keep nesting folders and you keep adding URL depth. There's no limit imposed by the framework beyond your filesystem and your own sanity managing the tree.

Colocating Files Without Creating Accidental Routes

Because only page and layout files are special, you're free to keep everything else a route needs right next to it. A common structure looks like this:

app/
  blog/
    page.tsx
    layout.tsx
    utils.ts
    posts.module.css
    components/
      PostCard.tsx
      PostList.tsx

None of utils.ts, posts.module.css, or the components/ folder become routes, because none of them are named page and — critically — components/ doesn't contain a page file of its own either. This is a meaningful upgrade over the Pages Router, where everything under pages/ was implicitly routable, forcing teams to keep components, hooks, and utilities in a completely separate top-level directory just to avoid accidentally exposing them as URLs.

If you want to be even more explicit about "this folder is never a route, don't even think about it," prefix the folder name with an underscore — _components/, _lib/ — which Next.js treats as a private folder, excluded from routing entirely regardless of what ends up inside it. This is mostly a documentation-as-code signal for your team rather than a functional necessity (a folder without a page file already isn't routable), but it removes any ambiguity for someone skimming the tree, and it protects you if you or a teammate later adds a file that happens to be named page.tsx inside a folder that was only ever meant to hold shared components.

Nesting Layouts

Layouts nest the same way pages do, and by default they nest automatically based on folder hierarchy — you don't have to manually compose them. Every layout wraps its children prop, which can be either a page or another, deeper layout. Add a layout.tsx inside app/blog/ and it slots in between the root layout and the blog page:

// app/blog/layout.tsx
export default function BlogLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return <section>{children}</section>;
}

With both layouts in place, the render tree for /blog/hello-world looks like this: root layout wraps blog layout, which wraps the blog post page. Each layout only needs to know about its own children — it has no idea what's ultimately being rendered inside that slot, whether that's the /blog index or a specific [slug] post. This is what makes the system compose cleanly: a layout three levels deep doesn't need to import or coordinate with the layouts above it.

The gotcha worth calling out: because nesting is automatic and implicit, it's easy to end up with a layout you didn't intend to affect a route. If you add a layout.tsx at app/(marketing)/layout.tsx intending it to apply to a couple of pages inside a route group, double check which folders actually sit under that group in your tree — a layout applies to everything beneath its folder, full stop, with no annotation needed and no annotation available to exclude a specific child.

Creating a Dynamic Segment

Dynamic segments let you generate routes from data instead of hand-authoring one folder per possible value. Wrap a folder name in square brackets — [slug], [id], [productHandle] — and Next.js treats that segment as a variable, populated by whatever value shows up in the URL at that position.

// app/blog/[slug]/page.tsx
export default async function BlogPostPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await getPost(slug);

  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </div>
  );
}

The detail that trips people up coming from earlier Next.js versions: params is a Promise, not a plain object. In older App Router releases params was synchronous, and a lot of tutorials, Stack Overflow answers, and AI-generated code out there still show { params: { slug } } as a destructured, non-async prop. On current versions that pattern throws — you have to await params (or, in a Server Component, use React's use() hook to unwrap it) before reading any property off it. If you're debugging a "params is not defined" or "cannot read properties of undefined" error after following an older guide, this is almost always the cause.

generateStaticParams (referenced but left empty in the nested-route example above) is the function you'd fill in to pre-render a known set of dynamic values at build time — say, every blog slug that exists at deploy time — rather than leaving every request to be rendered on demand.

Catch-all and optional catch-all segments

A single pair of square brackets captures exactly one segment, but sometimes you want a folder to swallow an arbitrary number of remaining path segments — a nested documentation tree, a file browser, a CMS-driven page hierarchy. Adding three dots inside the brackets turns a dynamic segment into a catch-all segment:

app/docs/[...slug]/page.tsx

This matches /docs/a, /docs/a/b, /docs/a/b/c, and so on, with slug arriving as an array (['a', 'b', 'c']) rather than a single string. Wrap the brackets a second time — [[...slug]] — and the segment becomes optional, so the same route also matches bare /docs with slug coming through as undefined. This distinction matters in practice: forgetting the double brackets means your route works for /docs/getting-started but 404s on /docs itself, which is an easy thing to overlook if you only tested nested paths during development.

Rendering with Search Params

Query strings — ?filters=price-desc, ?page=2 — are read differently depending on whether you're in a Server Component or a Client Component, and picking the wrong one has real performance consequences.

In a Server Component page, read them via the searchParams prop:

// app/page.tsx
export default async function Page({
  searchParams,
}: {
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
  const filters = (await searchParams).filters;
}

Like params, searchParams is a Promise you have to await. But there's a bigger consequence to using it than the syntax: reading searchParams opts your page into dynamic rendering. The reasoning is straightforward once you think about it — search params only exist on an actual incoming request, so a page can't be fully pre-rendered at build time if part of its output depends on them. If you've built a page you expected to be static and you're seeing it rendered on every request instead, check whether you're reading searchParams somewhere in that page's render path (including inside anything it awaits or imports).

In a Client Component, use the useSearchParams hook instead:

"use client";

import { useSearchParams } from "next/navigation";

export function FilterBar() {
  const searchParams = useSearchParams();
  const sort = searchParams.get("sort");

  return <p>Sorting by: {sort ?? "default"}</p>;
}

Choosing the right approach

  • Use the searchParams prop when the value needs to load data for the page — pagination, server-side filtering against a database, anything where the result of reading the param changes what you fetch.
  • Use useSearchParams when the param is client-only — filtering a list that's already been loaded via props, toggling a UI state that doesn't need a server round-trip.
  • Inside callbacks or event handlers, skip both hooks and just read new URLSearchParams(window.location.search) directly. This is a minor but real optimization: calling useSearchParams subscribes your component to param changes and can trigger re-renders you don't actually need inside a one-off click handler.

This three-way split rarely gets explained together, and most tutorials only show you one of the three, leaving you to rediscover the other two the hard way when the first one turns out to be the wrong tool for a given interaction.

Linking Between Pages

Routes are useless if users (and search engines) can't navigate between them without a full page reload. The <Link> component, imported from next/link, is how you do that — it extends the plain HTML <a> tag with prefetching and client-side transitions baked in.

// app/ui/post.tsx
import Link from "next/link";
import { getPosts } from "@/lib/posts";

export default async function Posts() {
  const posts = await getPosts();

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.slug}>
          <Link href={`/blog/${post.slug}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  );
}

Resist the urge to reach for onClick plus router.push() on a plain <div> or <button> just because it feels more familiar from other frameworks. <Link> renders a real anchor tag under the hood, which means it keeps working with "open in new tab," "copy link," screen readers, and crawlers indexing your site — none of which a JavaScript-only click handler gives you automatically. Reach for the useRouter hook instead of <Link> only when navigation needs to happen as a side effect of something else — after a form successfully submits, after an auth check passes — not as the primary way a user gets from one page to another.

Route Props Helpers

This is a newer addition that's easy to miss if you learned the App Router a version or two ago: Next.js now generates global TypeScript helper types — PageProps and LayoutProps — that infer the correct shape of params (and any named parallel-route slots) directly from your actual route structure.

// app/blog/[slug]/page.tsx
export default async function Page(props: PageProps<"/blog/[slug]">) {
  const { slug } = await props.params;
  return <h1>Blog post: {slug}</h1>;
}
// app/dashboard/layout.tsx
export default function Layout(props: LayoutProps<"/dashboard">) {
  return (
    <section>
      {props.children}
      {/* A folder like app/dashboard/@analytics would appear here, typed, as props.analytics */}
    </section>
  );
}

These types are generated automatically when you run next dev, next build, or the explicit next typegen command — there's nothing to import, and nothing to hand-write. Before this existed, it was extremely common to see params: { slug: string } hardcoded in a page's props type, which worked fine until someone renamed the folder from [slug] to [id] and the type silently went stale, with no compiler error pointing you at the mismatch. Typing against PageProps<'/blog/[slug]'> instead ties the type directly to the route that actually exists on disk — rename the folder, and the string literal argument stops matching, which is a compile error instead of a runtime surprise. For static routes with no dynamic segments, params simply resolves to {} — you don't need a separate code path for that case.

Common Mistakes Worth Knowing Ahead of Time

A few things that aren't wrong so much as unintuitive the first time you hit them:

Forgetting page.tsx makes a folder invisible, not broken. A folder with only a layout.tsx and no page.tsx doesn't error — it simply isn't a reachable route. This is actually useful for grouping shared layouts around several child routes without exposing the parent path itself, but it's confusing the first time you visit a URL expecting something and get a 404 with no obvious cause.

Client Components can't be async. Every example above that awaits data directly in the component body is a Server Component (the default). The moment you add "use client" to a file, you lose the ability to make that component's function async and fetch inside it directly — you're back to useEffect plus state, or a library like SWR or TanStack Query. Mixing this up is one of the most common "why won't my data fetching work" issues for anyone new to the App Router.

A layout re-rendering unexpectedly is almost always a key or route-structure issue, not a Next.js bug. If you notice a sidebar or header resetting its state on navigation when you expected it to persist, check whether the layout genuinely wraps both routes in question, or whether you've accidentally split it across two different folders that only look like they share a parent.

generateStaticParams without a fallback strategy can mean 404s for values you didn't pre-generate. If your dynamic segment needs to handle slugs that show up after the build (new blog posts published post-deploy, for instance), make sure you understand dynamicParams behavior on the route segment rather than assuming every possible value gets a page automatically.

Metadata lives in the same files but is a separate concern. Both page and layout files can export a metadata object or a generateMetadata function to control the page's <title>, description, and Open Graph tags. It's tempting to treat this article's examples as the full picture of what belongs in these files, but metadata deserves its own dedicated treatment — the merging behavior between a layout's metadata and its child page's metadata in particular has enough nuance (and enough SEO consequence if you get it wrong) that it's worth reading up on separately rather than guessing from a page/layout example that omits it.

A missing key prop on a list of <Link>s fails silently in ways that look like a routing bug. If you're mapping over data to render a list of linked items and React can't uniquely identify each one, you'll sometimes see stale content flash after navigating back to a list page — the instinct is to blame the App Router's caching, when the actual fix is just adding a stable key, usually the same identifier you're already using in the href.

Key Takeaways

ConceptFile / APIWhat it controls
Make a URL reachablepage.tsxRenders UI for one specific route segment
Share UI across routeslayout.tsxWraps children; preserves state across navigation
Required top-level layoutRoot layout.tsxMust include <html>/<body>; wraps the entire app
Multi-segment URLsNested foldersEach folder = one URL segment
Generate routes from data[segmentName] folderDynamic segment, read via params
Server-side query paramssearchParams propOpts the page into dynamic rendering
Client-side query paramsuseSearchParams hookRe-renders on param change; client-only
One-off param readsnew URLSearchParams(window.location.search)No hook subscription, no extra re-renders
Navigation between routes<Link> from next/linkPrefetching + client-side transitions
Type-safe route propsPageProps<...> / LayoutProps<...>Auto-generated, tied to your actual folder structure

Pages and layouts are the foundation everything else in the App Router builds on — route groups, parallel routes, and intercepting routes are all just more elaborate arrangements of the same two files. Get comfortable with how folders map to segments, how children composes through nested layouts, and the async nature of params and searchParams, and the more advanced routing patterns stop feeling like new concepts and start feeling like the same rules applied one layer deeper.

Tags :
Share :

Related Posts

Can Next.js Be Used with GraphQL?

Can Next.js Be Used with GraphQL?

Next.js and GraphQL are two powerful technologies that have gained significant traction in the web development community. Next.js, a React-based fram

Dive Deeper
How does Next.js differ from Create React App?

How does Next.js differ from Create React App?

In the world of modern web development, React.js has emerged as a dominant force due to its flexibility, performance, and extensive ecosystem. Two po

Dive Deeper
How does Next.js handle image optimization?

How does Next.js handle image optimization?

In modern web development, image optimization plays a critical role in enhancing user experience and improving site performance. Large, unoptimized i

Dive Deeper