Next.js
03 / 06

Server Components & App Router

Next.js Server Components & App Router

The App Router (introduced in Next.js 13) brings a new paradigm with React Server Components, streaming, and simplified data fetching.

File-based Routing

// App Router structure
app/
  layout.tsx              → Root layout (required)
  page.tsx                → / (home page)
  about/page.tsx          → /about
  blog/
    page.tsx              → /blog
    [slug]/page.tsx       → /blog/:slug
  dashboard/
    layout.tsx            → Layout for /dashboard/*
    page.tsx              → /dashboard
    settings/page.tsx     → /dashboard/settings
  api/
    users/route.ts        → /api/users (API route)

// app/page.tsx (home page)
export default function HomePage() {
  return (
    <div>
      <h1>Welcome to Next.js</h1>
    </div>
  );
}

// app/blog/[slug]/page.tsx (dynamic route)
export default function BlogPost({ params }: { params: { slug: string } }) {
  return (
    <article>
      <h1>Post: {params.slug}</h1>
    </article>
  );
}

Server Components vs Client Components

Server Components (default) render on the server, reducing JavaScript sent to client. Client Components use "use client" directive and can use hooks, events, and browser APIs.

// Server Component (default) - no "use client"
export default async function ServerComponent() {
  // Can access backend directly
  const data = await fetch('https://api.example.com/data');
  const posts = await data.json();
  
  // Can import server-only code
  import { db } from '@/lib/database';
  const users = await db.users.findMany();
  
  return (
    <div>
      <h1>Server Component</h1>
      {posts.map(post => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  );
}

// Client Component - needs "use client"
'use client';

import { useState, useEffect } from 'react';

export default function ClientComponent() {
  const [count, setCount] = useState(0);
  
  // Can use hooks, event handlers, browser APIs
  useEffect(() => {
    console.log('Count changed:', count);
  }, [count]);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

// Composition: Server Component using Client Component
import Counter from './Counter';  // Client component

export default async function Page() {
  const data = await fetchData();  // Server-side data fetch
  
  return (
    <div>
      <h1>{data.title}</h1>
      <Counter />  {/* Client component for interactivity */}
    </div>
  );
}

Layouts

// app/layout.tsx (root layout - required)
import type { Metadata } from 'next';
import './globals.css';

export const metadata: Metadata = {
  title: 'My Next.js App',
  description: 'Built with Next.js',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <header>
          <nav>{/* Global nav */}</nav>
        </header>
        <main>{children}</main>
        <footer>{/* Global footer */}</footer>
      </body>
    </html>
  );
}

// app/dashboard/layout.tsx (nested layout)
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="dashboard-layout">
      <aside>
        <nav>{/* Dashboard nav */}</nav>
      </aside>
      <div className="content">
        {children}
      </div>
    </div>
  );
}

Loading UI & Streaming

// app/dashboard/loading.tsx
export default function Loading() {
  return <div>Loading dashboard...</div>;
}

// app/error.tsx
'use client';

export default function Error({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <p>{error.message}</p>
      <button onClick={reset}>Try again</button>
    </div>
  );
}

// Streaming with Suspense
import { Suspense } from 'react';

export default function Page() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<p>Loading stats...</p>}>
        <Stats />  {/* Async component */}
      </Suspense>
      <Suspense fallback={<p>Loading posts...</p>}>
        <RecentPosts />
      </Suspense>
    </div>
  );
}

async function Stats() {
  const stats = await fetchStats();
  return <div>{stats.total} users</div>;
}

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free