Next.js App Router: The Mental Model
App Router confused me at first. Here's the mental model that made it click.
Server by Default
The key insight: in App Router, **everything is a Server Component by default**.
This is backwards from how we've thought about React for years.
The Two Worlds
Server Components
- Run on the server only
- Can access databases directly
- Can use `async/await` at the component level
- Cannot use hooks like `useState`, `useEffect`
- Cannot handle user events
// This runs on the server
async function UserList() {
const users = await db.users.findMany();
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
Client Components
- Run in the browser (and server for initial render)
- Can use hooks
- Can handle events
- Must opt-in with `'use client'` directive
'use client';
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
The Boundary
When you add `'use client'`, that component AND all its children become client components.
// This is a Server Component
function Page() {
return (
<div>
<ServerContent /> {/* Server */}
<InteractiveWidget /> {/* Client (and children) */}
</div>
);
}
Composition Pattern
Keep interactive parts small. Pass server data down:
// Server Component
async function ProductPage({ id }) {
const product = await db.products.find(id);
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
{/* Only the button needs to be client */}
<AddToCartButton productId={product.id} />
</div>
);
}
// Client Component - small and focused
'use client';
function AddToCartButton({ productId }) {
const [loading, setLoading] = useState(false);
const handleClick = async () => {
setLoading(true);
await addToCart(productId);
setLoading(false);
};
return <button onClick={handleClick}>{loading ? 'Adding...' : 'Add to Cart'}</button>;
}
Data Fetching
No more `useEffect` + `useState` for data:
// Old way (Client Component)
function Users() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch('/api/users').then(r => r.json()).then(setUsers);
}, []);
return /* ... */;
}
// New way (Server Component)
async function Users() {
const users = await db.users.findMany();
return /* ... */;
}
Less code, faster page loads, no loading states (for initial render).
Once this mental model clicks, App Router feels natural.