Next.js App Router: Server Components, Data Fetching, and Caching
Server Components vs Client Components
// app/users/page.tsx - Server Component (default)
// Runs on server, no client JavaScript
async function UsersPage() {
// Direct database access!
const users = await db.query('SELECT * FROM users');
return (
<div>
<h1>Users</h1>
{users.map(user => <UserCard key={user.id} user={user} />)}
</div>
);
}
// app/users/SearchBar.tsx - Client Component
'use client'; // Required for interactivity
import { useState } from 'react';
export function SearchBar({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState('');
// ... interactive code
}
Data Fetching Patterns
// Parallel data fetching
async function DashboardPage() {
// Both requests start simultaneously
const [user, orders] = await Promise.all([
fetchUser(),
fetchOrders(),
]);
return <Dashboard user={user} orders={orders} />;
}
// Sequential when data depends on previous
async function UserOrdersPage({ params }: { params: { userId: string } }) {
const user = await fetchUser(params.userId);
const orders = await fetchOrders(user.id); // needs user.id first
return <OrderList user={user} orders={orders} />;
}
Caching
// Default: cached indefinitely (static)
const data = await fetch('https://api.example.com/products');
// Revalidate every 60 seconds
const data = await fetch('https://api.example.com/products', {
next: { revalidate: 60 }
});
// No cache (dynamic)
const data = await fetch('https://api.example.com/user', {
cache: 'no-store'
});
// Route segment config
// app/dashboard/layout.tsx
export const revalidate = 300; // revalidate every 5 minutes
export const dynamic = 'force-dynamic'; // always dynamic
Server Actions
// app/users/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createUser(formData: FormData) {
const name = formData.get('name') as string;
const email = formData.get('email') as string;
// Server-side validation
if (!email.includes('@')) {
return { error: 'Invalid email' };
}
await db.insert('users', { name, email });
revalidatePath('/users'); // Invalidate cache
redirect('/users'); // Redirect after success
}
// app/users/new/page.tsx
import { createUser } from '../actions';
export default function NewUserPage() {
return (
<form action={createUser}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<button type="submit">Create User</button>
</form>
);
}
Streaming with Suspense
import { Suspense } from 'react';
async function SlowComponent() {
await new Promise(r => setTimeout(r, 2000)); // Simulate slow data fetch
const data = await fetchExpensiveData();
return <DataDisplay data={data} />;
}
export default function Page() {
return (
<div>
<h1>Dashboard</h1>
{/* Shows immediately */}
<QuickStats />
{/* Streams in when ready - shows skeleton while loading */}
<Suspense fallback={<Skeleton />}>
<SlowComponent />
</Suspense>
</div>
);
}
Middleware
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Auth check
const token = request.cookies.get('auth-token');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
// Add headers
const response = NextResponse.next();
response.headers.set('x-pathname', request.nextUrl.pathname);
return response;
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};
Next.js App Router with React Server Components represents a fundamental shift toward server-centric frontend development.