Layouts and Pages

Uses file-system based routing, meaning you can use folders and files to define routes. This page will guide you through how to create layouts and pages, and link between them.

Creating a page

A page is UI that is rendered on a specific route. To create a page, add a page file inside the app directory and default export a React component. For example, to create an index page (/):

app
page.tsx
/
app/page.tsx
export default function Page() {
  return <h1>Hello, World!</h1>
}

Creating a layout

A layout is UI that is shared between multiple pages. On navigation, layouts preserve state, remain interactive, and do not rerender.

You can define a layout by default exporting a React component from a layout file. The component should accept a children prop which can be a page or another layout. The layout in app/layout.tsx is called the root layout. It is defined at the root of the app directory and wraps all routes.

app
layout.tsx
page.tsx
/
app/layout.tsx
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <>
      {/* Layout UI */}
      <nav>Sidebar</nav>
      <main>{children}</main>
    </>
  )
}

Creating a nested route

A nested route is a route composed of multiple URL segments. For example, the /blog/[slug] route is composed of three segments:

  • / (Root Segment)
  • blog (Segment)
  • [slug] (Leaf Segment)
  • Folders are used to define the route segments that map to URL segments.
  • Files (like page and layout) are used to create UI that is shown for a segment.

To create nested routes, you can nest folders inside each other. For example, to add a route for /blog, create a folder called blog in the app directory:

app
blog
page.tsx
/blog
app/blog/page.tsx
export default function Page() {
  const posts = [
    { id: 1, title: 'Hello' },
    { id: 2, title: 'Getting Started' },
  ]
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

You can continue nesting folders to create nested routes. For example, to create a route for a specific blog post, create a new [slug] folder inside blog and add a page file:

app
blog
[slug]
page.tsx
/blog/:slug
app/blog/[slug]/page.tsx
export default function Page() {
  return <h1>Hello, Blog Post Page!</h1>
}

Nesting layouts

By default, layouts in the folder hierarchy are also nested, which means they wrap child layouts via their children prop. You can nest layouts by adding layout inside specific route segments (folders).

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

If you were to combine the two layouts above, the root layout (app/layout.tsx) would wrap the blog layout (app/blog/layout.tsx), which would wrap the blog (app/blog/page.tsx) and blog post page (app/blog/[slug]/page.tsx).

Creating a dynamic segment

Dynamic segments allow you to create routes that are generated from data. For example, instead of manually creating a route for each individual blog post, you can create a dynamic segment to generate the routes based on blog post data.

To create a dynamic segment, wrap the segment (folder) name in square brackets: [segmentName]. For example, in the app/blog/[slug]/page.tsx route, the [slug] is the dynamic segment.

app
blog
[slug]
page.tsx
/blog/hello-world
app/blog/[slug]/page.tsx
export default function BlogPostPage() {
  const { slug } = useParams<{ slug: string }>()

  return (
    <div>
      <h1>Blog Post: {slug}</h1>
    </div>
  )
}
Uses the useParams() hook (auto-imported). Supports [slug], [...slug] catch-all, and [[...slug]] optional catch-all.

Rendering with search params

You can access search parameters using the useSearchParams hook. It's auto-imported in all pages.

app/page.tsx
export default function Page() {
  const [searchParams] = useSearchParams()
  const filter = searchParams.get('filter')

  return <div>Filter: {filter}</div>
}

What to use and when

  • Use useSearchParams when you need search params to load data (pagination, filtering from API).
  • Use useSearchParams with client filtering (filtering a list already loaded).
  • As an optimization, you can use new URLSearchParams(window.location.search) in callbacks to read without re-renders.

Linking between pages

You can use the <Link> component to navigate between routes. <Link> is a built-in component that extends the HTML <a> tag to provide client-side navigation without full page reloads.

For example, to generate a list of blog posts, import <Link> (auto-imported) and pass a to prop:

app/blog/page.tsx
type Post = { slug: string; title: string }

export default function BlogList() {
  const posts: Post[] = [
    { slug: 'hello-world', title: 'Hello, World' },
    { slug: 'getting-started', title: 'Getting Started' },
  ]
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.slug}>
          <Link to={`/blog/${post.slug}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  )
}

While <Link> is ideal for declarative navigation in your JSX, sometimes you need to navigate programmatically. Bini.js provides the useNavigate hook for this.

Unlike <Link>, which renders an anchor tag, useNavigate returns a function you can call inside event handlers, effects, or after async operations. It performs a client-side navigation without a full page reload, preserving layout state and scroll position just like <Link>. This is perfect for post-form redirects, authentication flows, or conditional navigation where you need logic before navigating.

app/login/page.tsx
export default function LoginPage() {
  const navigate = useNavigate()

  const handleLogin = async () => {
    await login()
    // Redirect after successful login
    navigate('/dashboard')
  }

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    const formData = new FormData(e.target as HTMLFormElement)
    const result = await submitForm(formData)

    if (result.success) {
      navigate(`/blog/${result.slug}`)
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit" onClick={handleLogin}>Login</button>
    </form>
  )
}
Was this helpful?