The mental model shift
The App Router changed how Next.js thinks about components. The old model had one type of component — React components that ran on both server and client. The new model makes a distinction: by default, components are Server Components. To opt into client-side rendering and interactivity, you add 'use client' at the top of the file.
This matters because Server Components can do things client components cannot: directly await database calls, access server-side environment variables, and render without adding to the JavaScript bundle. Client Components can do things Server Components cannot: use React hooks, respond to user events, and maintain local state.
Server Components — the default, and when to stay there
If a component fetches data, renders static content, or does not need to respond to user interaction — keep it as a Server Component. The performance benefits are real: no client JS, no hydration cost, and data fetching happens at render time on the server.
// This is a Server Component — no 'use client' needed
// It can directly await data without useEffect or useState
export default async function ProductList() {
const products = await fetchProducts(); // runs on server
return (
<ul>
{products.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
);
}
Client Components — where to draw the boundary
Add 'use client' when the component needs hooks, browser APIs, event handlers, or local state. The key principle is to push the boundary as far down the component tree as possible. A page can be a Server Component that passes data as props to a small interactive Client Component at the leaf level.
'use client';
// Only this button is a Client Component — the page stays server-rendered
export function AddToCartButton({ productId }: { productId: string }) {
const [added, setAdded] = useState(false);
return (
<button onClick={() => { addToCart(productId); setAdded(true); }}>
{added ? 'Added' : 'Add to cart'}
</button>
);
}
Layouts and nested routes
The layout.tsx file wraps all routes in its directory and below. A layout persists across route changes — it does not unmount and remount. This makes it the right place for navigation, persistent UI, and things that should survive page transitions.
The template.tsx alternative remounts on every navigation — useful when you specifically want reset behaviour between routes, like running entry animations on every page.
Data fetching — the straightforward version
In the App Router, you fetch data directly in async Server Components. Next.js extends fetch with caching and revalidation options:
// Cached indefinitely (like getStaticProps)
const data = await fetch(url, { cache: 'force-cache' });
// Not cached (like getServerSideProps)
const data = await fetch(url, { cache: 'no-store' });
// Revalidate every 60 seconds (like ISR)
const data = await fetch(url, { next: { revalidate: 60 } });
Server Actions
Server Actions let you run server-side code in response to form submissions or button clicks without writing a separate API route. They are async functions marked with 'use server' that can be called directly from components.
// In a separate file marked 'use server', or inline in a Server Component
async function submitForm(formData: FormData) {
'use server';
const name = formData.get('name');
await db.insert({ name }); // runs on server
}
// Used in a form — no API route needed
<form action={submitForm}>
<input name="name" />
<button type="submit">Save</button>
</form>
Loading and error states
loading.tsx in any route segment renders while the page is fetching. It wraps the page in a Suspense boundary automatically. error.tsx renders if an unhandled error occurs in the route — it receives the error and a reset function as props.
Both files are Client Components by requirement — they respond to runtime state that only exists in the browser.
Metadata
Export a metadata object or a generateMetadata async function from any page.tsx or layout.tsx to set <title>, description, Open Graph tags, and more. The metadata from nested routes merges with and can override the root layout metadata.
export const metadata: Metadata = {
title: 'Product Page',
description: 'Description for this product',
};