Next.js is the leading React meta-framework. The App Router (Next.js 13+) introduces React Server Components, enabling server-side rendering without client-side hydration for data-fetching components. Understanding Server vs Client components, streaming, and caching is essential for modern Next.js development.

Key Points

  • App Router: file-system routing in app/ — page.tsx, layout.tsx, loading.tsx, error.tsx, not-found.tsx conventions
  • Server Components (default): render on server, zero client JS — cannot use useState, useEffect, browser APIs, event handlers
  • Client Components: "use client" directive — have interactivity, hooks, event handlers; hydrated in the browser
  • Data fetching in Server Components: async function component + await fetch() — fetch is extended with Next.js caching
  • Caching: fetch() in Server Components is cached by default — revalidate with next: { revalidate: 60 } or force-dynamic
  • Streaming: <Suspense> in Server Components enables HTML streaming — page shell renders immediately, data arrives progressively
  • Route handlers: app/api/route.ts — GET, POST, etc. — replaces pages/api; edge runtime support
  • Server Actions: async functions marked "use server" — called from client without API routes; forms with server mutation
  • Image, Font, Link: Next.js optimised components — automatic WebP, font subsetting, prefetching
FeatureServer ComponentClient Component
useState / useEffectNoYes
Event handlers (onClick)NoYes
fetch / DB / secretsYesNo (exposes to browser)
Sent to client as JSNo (HTML only)Yes (hydrated)
Access to browser APIsNoYes
Default in App RouterYesRequires "use client"

Next.js App Router: Server Components + Suspense streaming, Client Components, Server Actions, ISR with revalidate

// app/users/page.tsx — Server Component (no "use client")
async function UsersPage() {
  // Direct DB query or fetch — runs on server only
  const users = await db.user.findMany({ orderBy: { name: 'asc' } });

  return (
    <main>
      <h1>Users</h1>
      <Suspense fallback={<UserListSkeleton />}>
        {/* Streaming: UserStats fetches independently */}
        <UserStats />
      </Suspense>
      <ul>
        {users.map(user => (
          <li key={user.id}>
            {user.name}
            <DeleteButton userId={user.id} />  {/* Client Component for interactivity */}
          </li>
        ))}
      </ul>
    </main>
  );
}

// app/users/DeleteButton.tsx — Client Component
'use client';
import { useTransition } from 'react';
import { deleteUser } from './actions';   // Server Action

export function DeleteButton({ userId }: { userId: string }) {
  const [isPending, startTransition] = useTransition();
  return (
    <button
      disabled={isPending}
      onClick={() => startTransition(() => deleteUser(userId))}
    >
      {isPending ? 'Deleting...' : 'Delete'}
    </button>
  );
}

// app/users/actions.ts — Server Action
'use server';
import { revalidatePath } from 'next/cache';

export async function deleteUser(id: string) {
  await db.user.delete({ where: { id } });
  revalidatePath('/users');   // invalidate cache, triggers re-render
}

// app/users/[id]/page.tsx — dynamic route with caching
async function UserPage({ params }: { params: { id: string } }) {
  const user = await fetch(`https://api.example.com/users/${params.id}`, {
    next: { revalidate: 3600 }   // ISR: cached for 1 hour, revalidated in background
  }).then(r => r.json());
  return <UserProfile user={user} />;
}

// Generate static params for SSG
export async function generateStaticParams() {
  const users = await db.user.findMany({ select: { id: true } });
  return users.map(u => ({ id: u.id }));
}

Real-World Example

Server Components eliminate the client-server waterfall: instead of render → JS loads → fetch → re-render, the server renders HTML with data already in it. A typical product page that previously needed 3 client fetches (product, reviews, recommendations) can now be a Server Component with 3 parallel awaits — delivered as a single HTML response with no client JS for the fetching logic. The bundle size reduction is significant for performance on slow networks.