DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Next.js App Router Patterns: Layouts, Loading States, and Parallel Routes

Next.js App Router Patterns: Layouts, Loading States, and Parallel Routes

The App Router introduced powerful primitives that most tutorials skip.
Here are the patterns that actually matter in production.

Layout Nesting

app/
  layout.tsx          # root layout (html, body)
  page.tsx            # /
  dashboard/
    layout.tsx        # shared sidebar + nav
    page.tsx          # /dashboard
    settings/
      layout.tsx      # settings-specific tabs
      page.tsx        # /dashboard/settings
Enter fullscreen mode Exit fullscreen mode
// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="flex">
      <Sidebar />
      <main className="flex-1 p-8">{children}</main>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Layouts persist across navigations — the sidebar doesn't re-render when you navigate between dashboard pages.

Loading UI

// app/dashboard/loading.tsx
export default function DashboardLoading() {
  return (
    <div className="space-y-4">
      <Skeleton className="h-8 w-48" />
      <Skeleton className="h-32 w-full" />
      <Skeleton className="h-64 w-full" />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

This automatically wraps page.tsx in a Suspense boundary. No manual <Suspense> needed.

Parallel Routes

Render multiple pages simultaneously in the same layout:

app/dashboard/
  layout.tsx
  @analytics/
    page.tsx
  @revenue/
    page.tsx
  @users/
    page.tsx
Enter fullscreen mode Exit fullscreen mode
// app/dashboard/layout.tsx
export default function DashboardLayout({
  analytics,
  revenue,
  users,
  children,
}: {
  analytics: React.ReactNode
  revenue: React.ReactNode
  users: React.ReactNode
  children: React.ReactNode
}) {
  return (
    <div className="grid grid-cols-3 gap-4">
      {analytics}
      {revenue}
      {users}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Each slot loads independently — analytics doesn't block revenue from rendering.

Intercepting Routes

Show a modal when navigating to a URL, but the full page on direct visit:

app/
  feed/
    page.tsx
    @modal/
      (.)photo/[id]/
        page.tsx    # intercepts /photo/[id] from feed
  photo/
    [id]/
      page.tsx      # full page on direct visit
Enter fullscreen mode Exit fullscreen mode

Instagram-style photo modals. Same URL, different UI depending on how you got there.

Server vs Client Components

// Server Component (default) — runs on server, no JS shipped
async function ProductList() {
  const products = await db.product.findMany()  // direct DB access!
  return (
    <ul>
      {products.map(p => (
        <li key={p.id}>
          <ProductCard product={p} />  {/* can be client component */}
        </li>
      ))}
    </ul>
  )
}

// Client Component — add interactivity
'use client'

function ProductCard({ product }: { product: Product }) {
  const [saved, setSaved] = useState(false)
  return (
    <div>
      <h3>{product.name}</h3>
      <button onClick={() => setSaved(!saved)}>
        {saved ? 'Saved' : 'Save'}
      </button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Server Actions

// Server action — runs on the server, called from client
async function createPost(formData: FormData) {
  'use server'
  const title = formData.get('title') as string
  await db.post.create({ data: { title, authorId: await getUserId() } })
  revalidatePath('/posts')
}

// Use in a form — no API route needed
export default function NewPostForm() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="Post title" />
      <button type="submit">Create Post</button>
    </form>
  )
}
Enter fullscreen mode Exit fullscreen mode

Route Groups

Organize routes without affecting the URL:

app/
  (marketing)/
    layout.tsx      # marketing layout
    page.tsx        # /
    pricing/
      page.tsx      # /pricing
  (app)/
    layout.tsx      # authenticated app layout
    dashboard/
      page.tsx      # /dashboard
    settings/
      page.tsx      # /settings
Enter fullscreen mode Exit fullscreen mode

Marketing pages and app pages share a domain but different layouts — no URL impact.


The AI SaaS Starter Kit uses all these patterns: route groups for marketing vs app, parallel routes for the dashboard, and Server Actions for forms. $99 one-time — production-ready from day one.

Top comments (0)