Next.js 15 - Server Components, App Router, and When to Actually Use Them

Beyond the Hype - A Practical Guide to Modern Next.js

Server Components are the future. App Router is the new standard. But should you use them, and how do you avoid breaking everything?

The Migration That Broke Everything

Updated a Next.js 12 app to Next.js 15 with App Router. Build succeeded. App loaded. Then users complained:

“Forms don’t work. Buttons don’t respond. Nothing happens when I click.”

I forgot to add ‘use client’ to interactive components.

Server Components rendered fine. But all my onClick, useState, useEffect code silently failed.

You need to understand what goes where.

What Changed

The Old Way (Pages Router)

Everything was a hybrid - server-rendered then client-hydrated. Simple mental model.

The New Way (App Router)

Default = Server Component. No JavaScript sent to client unless you explicitly opt in.

Server Components vs Client Components

Server Components (Default)

Can do:

  • Fetch data directly
  • Access databases
  • Read environment variables
  • Use server-only libraries

Cannot do:

  • Use hooks (useState, useEffect, etc.)
  • Handle events (onClick, onChange, etc.)
  • Use browser APIs (window, localStorage, etc.)

Example:

// app/users/page.tsx (Server Component by default)
async function UsersPage() {
  const users = await fetch('https://api.example.com/users')
    .then(r => r.json());

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Client Components (Opt-in with ‘use client’)

'use client';
import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

The Mental Model That Actually Works

Server Components = Your Backend: Fetch data, process logic, return HTML.

Client Components = Your Frontend: Handle user input, manage UI state, add interactivity.

// app/page.tsx (Server Component)
async function HomePage() {
  const users = await fetchUsers();

  return (
    <div>
      <SearchBar />  {/* Client Component */}
      <UserList users={users} />  {/* Server Component */}
    </div>
  );
}

Data Fetching - The New Way

No More getServerSideProps

// OLD
export async function getServerSideProps() {
  const data = await fetchData();
  return { props: { data } };
}

// NEW - Just fetch directly
async function Page() {
  const data = await fetchData();
  return <div>{data}</div>;
}

Caching and Revalidation

// Cached by default
const data = await fetch('https://api.example.com/data');

// Force fresh
const data = await fetch('https://api.example.com/data', {
  cache: 'no-store'
});

// Revalidate after 60 seconds
const data = await fetch('https://api.example.com/data', {
  next: { revalidate: 60 }
});

Loading States and Error Handling

loading.tsx: Auto-shows while page fetches data.

error.tsx: Automatic error boundary.

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

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

not-found.tsx: Shows 404 pages.

When to Use Server vs Client Components

Use Server Components for:

  • Fetching data
  • Accessing backend directly
  • SEO-critical content
  • Reducing JavaScript bundle

Use Client Components for:

  • Interactivity (clicks, input)
  • State management (useState)
  • Effects (useEffect)
  • Browser APIs (localStorage, window)

Common Pitfalls

1. Forgetting ‘use client’

// This will fail silently
export default function Button() {
  const [clicked, setClicked] = useState(false);
  return <button onClick={() => setClicked(true)}>Click</button>;
}

// Add 'use client'
'use client';
export default function Button() {
  const [clicked, setClicked] = useState(false);
  return <button onClick={() => setClicked(true)}>Click</button>;
}

2. Passing Functions to Server Components

Can’t pass functions from Client to Server. Use children instead.

3. Using Client-Only Libraries in Server Components

Use dynamic import with ssr: false:

import dynamic from 'next/dynamic';

const Chart = dynamic(() => import('./Chart'), {
  ssr: false
});

Is App Router Worth It?

Use App Router if:

  • Starting a new project
  • Want better performance
  • Need streaming/suspense
  • SEO is critical

Stay on Pages Router if:

  • Existing app works fine
  • Team isn’t ready for shift
  • Migration cost is too high

For most new projects? Use App Router. It’s the future.


Server Components are confusing at first. The mental model takes time to click.

But once it does? You’ll write faster, more efficient apps with less code.

Start small. Try it on a new feature. Don’t rush migration.

The future is here. And it’s actually pretty good.