Next.js 14: Complete Guide for React Developers
Moving from Create React App to Next.js felt like upgrading from a bicycle to a motorcycle. More power, but also more things that can go wrong. Here's everything I learned.
Why Next.js?
- Server-Side Rendering - Better SEO, faster initial load
- File-based routing - No react-router configuration
- API routes - Backend in the same project
- Built-in optimization - Images, fonts, scripts
App Router vs Pages Router
Next.js 14 uses the App Router by default. Key differences:
pages/ → app/
pages/index.js → app/page.js
pages/about.js → app/about/page.js
pages/blog/[slug].js → app/blog/[slug]/page.js
Server vs Client Components
Server Components (default in App Router):
- Run on server
- Can access database directly
- Smaller bundle size
- No interactivity
Client Components:
- Run in browser
- Needed for interactivity
- Use "use client" directive
// Server Component (default)
async function ProductList() {
const products = await db.products.findMany();
return (
<ul>
{products.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
);
}
// Client Component
"use client"
function AddToCartButton({ productId }) {
const [loading, setLoading] = useState(false);
const handleClick = async () => {
setLoading(true);
await addToCart(productId);
setLoading(false);
};
return <button onClick={handleClick} disabled={loading}>Add to Cart</button>;
}
Data Fetching
In Server Components
// No useEffect needed!
async function UserProfile({ userId }) {
const user = await fetch(`/api/users/${userId}`);
return <h1>{user.name}</h1>;
}
Caching
// Cached (default)
fetch('https://api.example.com/data');
// Revalidate every 60 seconds
fetch('https://api.example.com/data', { next: { revalidate: 60 } });
// No cache
fetch('https://api.example.com/data', { cache: 'no-store' });
API Routes
// app/api/users/route.js
import { NextResponse } from 'next/server';
export async function GET(request) {
const users = await db.users.findMany();
return NextResponse.json(users);
}
export async function POST(request) {
const body = await request.json();
const user = await db.users.create({ data: body });
return NextResponse.json(user, { status: 201 });
}
Layouts and Templates
// app/layout.js - Wraps entire app
export default function RootLayout({ children }) {
return (
<html>
<body>
<Navbar />
{children}
<Footer />
</body>
</html>
);
}
// app/dashboard/layout.js - Nested layout
export default function DashboardLayout({ children }) {
return (
<div className="dashboard">
<Sidebar />
<main>{children}</main>
</div>
);
}
Loading and Error States
// app/products/loading.js
export default function Loading() {
return <ProductSkeleton />;
}
// app/products/error.js
"use client"
export default function Error({ error, reset }) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
Metadata for SEO
// app/blog/[slug]/page.js
export async function generateMetadata({ params }) {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
images: [post.coverImage],
},
};
}
Deployment
Vercel (best experience):
npm install -g vercel
vercel
Self-hosted:
npm run build
npm start
Common Gotchas
- Client/Server boundary - You'll forget "use client" a lot
- Hydration errors - Server and client must render same HTML
- Fetch caching - Aggressive by default, can cause stale data
Next.js has a learning curve, but the developer experience and performance benefits are worth it.