Next.js Guide
Next.js is a production-ready React framework that provides server-side rendering, static site generation, and many other features out of the box. This guide focuses on Next.js 15+ with the modern App Router.
When to use Next.js?
- Need SEO and faster initial page loads
- Want built-in optimization (images, fonts, scripts)
- Building full-stack applications with API routes
- Want file-based routing and automatic code splitting
Why We Use Next.js at GDG Algiers
Section titled “Why We Use Next.js at GDG Algiers”At GDG Algiers, we choose Next.js for several of our projects because:
- SEO Requirements: Our event websites, blogs, and documentation sites need excellent search engine visibility
- Performance: Initial page loads are significantly faster with SSR/SSG compared to client-side React
- Full-Stack Capability: API routes eliminate the need for separate backend services for simple applications
- Developer Experience: Built-in optimizations, TypeScript support, and excellent tooling out of the box
- Deployment Simplicity: Seamless deployment to Vercel, Netlify, or any Node.js hosting platform
When we use Next.js vs React:
- Next.js: Marketing sites, blogs, documentation, e-commerce, dashboards with SEO needs
- React SPA: Admin panels, internal tools, highly interactive apps where SEO isn’t critical
App Router vs Pages Router
Section titled “App Router vs Pages Router”Next.js evolved from the Pages Router to the more powerful App Router. Here’s what you need to know:
// app/page.tsxexport default function HomePage() { return <h1>Welcome to Next.js 15!</h1>;}
// app/about/page.tsxexport default function AboutPage() { return <h1>About Us</h1>;}
// app/layout.tsx// Root layoutexport default function RootLayout({ children,}: { children: React.ReactNode;}) { return ( <html lang="en"> <body>{children}</body> </html> );}
Benefits:
- Server Components by default
- Improved performance and SEO
- Better developer experience
- More flexible layouts
// pages/index.tsxexport default function HomePage() { return <h1>Welcome to Next.js!</h1>;}
// pages/about.tsxexport default function AboutPage() { return <h1>About Us</h1>;}
// pages/_app.tsx// App wrapperexport default function App({ Component, pageProps }) { return <Component {...pageProps} />;}
Still works but:
- Client-side rendering by default
- Less optimized bundle sizes
- Limited layout flexibility
Server Components vs Client Components
Section titled “Server Components vs Client Components”Next.js 15+ introduces React Server Components by default, enabling better performance and SEO.
Server Components (Default)
Section titled “Server Components (Default)”Server Components run on the server and are great for data fetching and SEO:
// app/products/page.tsx// Server Component (no 'use client' needed)import { getProducts } from '@/lib/api';
export default async function ProductsPage() { // This runs on the server - direct database access possible const products = await getProducts();
return ( <div> <h1>Products</h1> {products.map(product => ( <div key={product.id}> <h2>{product.name}</h2> <p>${product.price}</p> </div> ))} </div> );}
Server Components are perfect for:
- Data fetching from databases or APIs
- Rendering static content
- Heavy computations
- Using environment variables securely
Client Components
Section titled “Client Components”Add 'use client'
when you need browser features or interactivity:
// AddToCart.tsx'use client'; // Required directive
import { useState } from 'react';
export default function AddToCartButton({ productId }) { const [isLoading, setIsLoading] = useState(false);
const handleAddToCart = async () => { setIsLoading(true); try { await fetch('/api/cart', { method: 'POST', body: JSON.stringify({ productId }), }); } finally { setIsLoading(false); } };
return ( <button onClick={handleAddToCart} disabled={isLoading}> {isLoading ? 'Adding...' : 'Add to Cart'} </button> );}
Client Components are needed for:
- Event handlers (onClick, onChange, onSubmit)
- State and effects (useState, useEffect)
- Browser APIs (localStorage, geolocation)
- Interactive libraries
Data Fetching
Section titled “Data Fetching”Next.js offers multiple rendering strategies, each with different performance and user experience characteristics:
Rendering Strategies Explained
Section titled “Rendering Strategies Explained”- SSG (Static Site Generation): Pages are built at build time and served as static files. Best for content that doesn’t change frequently (blogs, documentation)
- SSR (Server-Side Rendering): Pages are rendered on the server for each request. Best for personalized content or frequently changing data
- ISR (Incremental Static Regeneration): Combines benefits of SSG and SSR - static pages that can be updated in the background. Perfect for content that updates periodically
Basic Server-Side Data Fetching
Section titled “Basic Server-Side Data Fetching”// app/users/page.tsxasync function getUsers() { const res = await fetch('https://api.example.com/users', { cache: 'force-cache', // Cache indefinitely (SSG behavior) });
if (!res.ok) { throw new Error('Failed to fetch users'); }
return res.json();}
export default async function UsersPage() { const users = await getUsers();
return ( <div> <h1>Users ({users.length})</h1> {users.map(user => ( <div key={user.id}> <h3>{user.name}</h3> <p>{user.email}</p> </div> ))} </div> );}
Cache Control Options
Section titled “Cache Control Options”// Static Site Generation (SSG) - Built at build time, cached indefinitely// Perfect for: blogs, documentation, marketing pagesfetch('https://api.example.com/posts', { cache: 'force-cache'});
// Server-Side Rendering (SSR) - Rendered on each request// Perfect for: user dashboards, personalized content, real-time datafetch('https://api.example.com/user-profile', { cache: 'no-store'});
// Incremental Static Regeneration (ISR) - Static with background updates// Perfect for: product catalogs, news sites, content that updates periodicallyfetch('https://api.example.com/products', { next: { revalidate: 60 } // Revalidate every 60 seconds});
// On-demand revalidation - Trigger updates when content changesfetch('https://api.example.com/posts', { next: { tags: ['posts'] } // Can be revalidated using revalidateTag('posts')});
TanStack Query with Next.js
Section titled “TanStack Query with Next.js”TanStack Query works excellently with Next.js for client-side data management while leveraging server-side rendering.
Server State Hydration
Section titled “Server State Hydration”Server State Hydration is a powerful pattern that combines server-side data fetching with client-side state management. Here’s how it works:
- Server prefetches data during SSR/SSG
- Data is serialized and sent to the client
- TanStack Query hydrates the cache with server data
- Client takes over for subsequent interactions
This eliminates loading states on initial render while maintaining the benefits of client-side caching.
// app/posts/page.tsx// Server Componentimport { dehydrate, QueryClient } from '@tanstack/react-query';import { HydrationBoundary } from '@tanstack/react-query';import PostsList from './posts-list';
async function getPosts() { const res = await fetch('https://api.example.com/posts'); return res.json();}
export default async function PostsPage() { const queryClient = new QueryClient();
// Prefetch data on server - this runs during SSR await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, });
return ( <HydrationBoundary state={dehydrate(queryClient)}> <PostsList /> </HydrationBoundary> );}
// posts-list.tsx// Client Component'use client';
import { useQuery } from '@tanstack/react-query';
export default function PostsList() { // This query starts with server data, no loading state on initial render const { data: posts, isLoading, error } = useQuery({ queryKey: ['posts'], queryFn: () => fetch('/api/posts').then(res => res.json()), staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes });
// isLoading will be false on initial render due to hydration if (isLoading) return <div>Loading posts...</div>; if (error) return <div>Error: {error.message}</div>;
return ( <div> {posts.map(post => ( <article key={post.id}> <h2>{post.title}</h2> <p>{post.excerpt}</p> </article> ))} </div> );}
Benefits of this pattern:
- No loading spinners on initial page load
- SEO-friendly content is immediately available
- Client-side caching for smooth navigation
- Background refetching for data freshness
Essential Features
Section titled “Essential Features”Image Optimization
Section titled “Image Optimization”Next.js Image component automatically optimizes images:
import Image from 'next/image';
export default function ProfilePage() { return ( <div> {/* Basic optimized image */} <Image src="/profile.jpg" alt="Profile picture" width={300} height={300} priority // Load immediately for above-the-fold images />
{/* Responsive image that fills container */} <div style={{ position: 'relative', width: '100%', height: '400px' }}> <Image src="/hero.jpg" alt="Hero image" fill style={{ objectFit: 'cover' }} sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" /> </div> </div> );}
API Routes
Section titled “API Routes”// app/api/users/route.tsimport { NextResponse } from 'next/server';
// GET /api/usersexport async function GET() { try { const users = await getUsersFromDatabase(); return NextResponse.json(users); } catch (error) { return NextResponse.json( { error: 'Failed to fetch users' }, { status: 500 } ); }}
// POST /api/usersexport async function POST(request: Request) { try { const body = await request.json(); const newUser = await createUser(body); return NextResponse.json(newUser, { status: 201 }); } catch (error) { return NextResponse.json( { error: 'Failed to create user' }, { status: 500 } ); }}
Middleware
Section titled “Middleware”Middleware is the recommended approach for protected routes in Next.js because it runs at the edge before any rendering occurs, making it more efficient than component-level authentication checks.
Why middleware for authentication?
- Performance: Redirects happen before page rendering, saving server resources
- Security: No risk of protected content being briefly visible before redirect
- Consistency: Single place to handle authentication logic across your entire app
- Edge optimization: Runs at CDN edge locations for faster response times
// middleware.ts (in project root)import { NextResponse } from 'next/server';import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) { // Check authentication for protected routes const token = request.cookies.get('auth-token')?.value; const isAuthPage = request.nextUrl.pathname.startsWith('/login'); const isProtectedRoute = request.nextUrl.pathname.startsWith('/dashboard');
// Redirect to login if accessing protected route without token if (isProtectedRoute && !token) { return NextResponse.redirect(new URL('/login', request.url)); }
// Redirect to dashboard if logged in user tries to access login page if (isAuthPage && token) { return NextResponse.redirect(new URL('/dashboard', request.url)); }
return response;}
export const config = { matcher: [ // Match all paths except static files and API routes '/((?!api|_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', ],};
Production Configuration
Section titled “Production Configuration”next.config.js
Section titled “next.config.js”/** @type {import('next').NextConfig} */const nextConfig = { // Image optimization settings images: { domains: ['example.com', 'cdn.example.com'], formats: ['image/webp', 'image/avif'], },
// Enable compression compress: true,};
module.exports = nextConfig;
Best Practices
Section titled “Best Practices”When to Use What
Section titled “When to Use What”- Server Components: Data fetching, static content, SEO-critical pages
- Client Components: Interactive features, forms, state management
- API Routes: Backend logic, database operations, third-party integrations
- Middleware: Authentication, redirects, request/response modification
Performance Tips
Section titled “Performance Tips”- Use the
next/image
component for all images - Implement proper caching strategies based on data freshness needs
- Keep client boundaries low in your component tree
- Use dynamic imports for heavy components
- Monitor Core Web Vitals with Next.js Analytics
Common Patterns
Section titled “Common Patterns”// Combining Server and Client Componentsexport default async function ProductPage({ params }) { // Server Component - fetch data const product = await getProduct(params.id);
return ( <div> <h1>{product.name}</h1> <p>{product.description}</p>
{/* Client Component for interactivity */} <AddToCartButton productId={product.id} /> <ProductReviews productId={product.id} /> </div> );}
Next.js 15+ with App Router provides a solid foundation for building fast, SEO-friendly React applications. The combination of Server Components, optimized data fetching, and tools like TanStack Query creates an excellent developer experience while maintaining great performance.
Useful Resources: