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.