Next.js revolutionized the way React applications are built, offering different rendering strategies that can be chosen depending on the page characteristics and project requirements. In this module, we will discuss how rendering strategies work in the context of the App Router, introduced in Next.js 13 and developed in subsequent versions.
Next.js has undergone a significant evolution in its approach to rendering. In versions before 13, we used special functions like
getStaticProps, getServerSideProps, or getStaticPaths to determine the rendering strategy for a given page. Each page had to choose one specific strategy.In the App Router, the approach is different - rendering is now more granular and based on the concept of React Server Components. Instead of determining a strategy for the entire page, we can mix different approaches at the level of individual components.
At the core of the new rendering model are React Server Components (RSC), which have changed the way we think about React applications. The main features of RSC are:
In the App Router, we can still use traditional rendering strategies, but the way they are implemented is different. Let's look at each one.
In the App Router, SSR is the default behavior for Route Handlers and Server Components.
1// app/products/page.tsx
2import { Product } from '@/components/Product';
3
4async function getData() {
5 // This function runs for every request on the server
6 const res = await fetch('https://api.example.com/products', { cache: 'no-store' });
7
8 if (!res.ok) {
9 throw new Error('Failed to fetch data');
10 }
11
12 return res.json();
13}
14
15export default async function ProductsPage() {
16 const products = await getData();
17
18 return (
19 <div>
20 <h1>Our Products</h1>
21 <div className="grid">
22 {products.map(product => (
23 <Product key={product.id} data={product} />
24 ))}
25 </div>
26 </div>
27 );
28}Key features of SSR in App Router:
cache: 'no-store' option in the fetch function or revalidate: 0 to indicate that data should be fetched on every requestasync), which allows direct use of await in the component bodyAdvantages of SSR:
Disadvantages of SSR:
In the App Router, SSG is achieved through the default behavior of
fetch without the cache: 'no-store' option.1// app/blog/page.tsx
2import { BlogPost } from '@/components/BlogPost';
3
4async function getData() {
5 // This function runs only at build time (or on demand in dev mode)
6 const res = await fetch('https://api.example.com/blog-posts');
7
8 if (!res.ok) {
9 throw new Error('Failed to fetch data');
10 }
11
12 return res.json();
13}
14
15export default async function BlogPage() {
16 const posts = await getData();
17
18 return (
19 <div>
20 <h1>Our Blog</h1>
21 <div className="grid">
22 {posts.map(post => (
23 <BlogPost key={post.id} data={post} />
24 ))}
25 </div>
26 </div>
27 );
28}Key features of SSG in App Router:
getStaticProps - a regular fetch is sufficientAdvantages of SSG:
Disadvantages of SSG:
In the App Router, ISR is achieved by adding the
revalidate option to the fetch function or to the route segment.1// app/products/[id]/page.tsx
2import { ProductDetails } from '@/components/ProductDetails';
3import { notFound } from 'next/navigation';
4
5// Option 1: Revalidation at the page level
6export const revalidate = 3600; // Revalidation every hour
7
8async function getProduct(id) {
9 // Option 2: Revalidation at the fetch function level
10 const res = await fetch(`https://api.example.com/products/${id}`, {
11 next: { revalidate: 3600 } // Also every hour
12 });
13
14 if (!res.ok) {
15 return null;
16 }
17
18 return res.json();
19}
20
21export default async function ProductPage({ params }) {
22 const product = await getProduct((await params).id);
23
24 if (!product) {
25 notFound();
26 }
27
28 return <ProductDetails product={product} />;
29}Key features of ISR in App Router:
revalidate variablefetch request level using the next: { revalidate: seconds } optionAdvantages of ISR:
Disadvantages of ISR:
In the App Router, CSR is achieved by marking a component as a "Client Component" using the
'use client' directive at the beginning of the file.1'use client';
2
3// app/dashboard/page.tsx
4import { useState, useEffect } from 'react';
5
6export default function Dashboard() {
7 const [data, setData] = useState(null);
8 const [isLoading, setIsLoading] = useState(true);
9
10 useEffect(() => {
11 async function fetchData() {
12 try {
13 const res = await fetch('/api/dashboard-data');
14 const jsonData = await res.json();
15 setData(jsonData);
16 } catch (error) {
17 console.error('Error fetching data:', error);
18 } finally {
19 setIsLoading(false);
20 }
21 }
22
23 fetchData();
24 }, []);
25
26 if (isLoading) return <div>Loading data...</div>;
27
28 return (
29 <div>
30 <h1>Control Panel</h1>
31 {/* Rendering data fetched on the client side */}
32 {data && data.map(item => (
33 <div key={item.id}>{item.name}</div>
34 ))}
35 </div>
36 );
37}Key features of CSR in App Router:
'use client' directiveuseState, useEffect, etc.)Advantages of CSR:
Disadvantages of CSR:
One of the greatest advantages of the App Router is the ability to mix different rendering strategies within a single application, and even a single page. This allows us to choose the optimal strategy for each component.
Example of using mixed strategies:
1// app/products/[category]/page.tsx
2import { ProductList } from '@/components/ProductList';
3import { CategoryHeader } from '@/components/CategoryHeader';
4import FilterSidebar from '@/components/FilterSidebar'; // Client Component
5
6// This page and its data are rendered statically
7// with revalidation every hour
8export const revalidate = 3600;
9
10// We specify which categories should be generated statically
11export async function generateStaticParams() {
12 const categories = await fetch('https://api.example.com/categories').then(res => res.json());
13
14 return categories.map(category => ({
15 category: category.slug,
16 }));
17}
18
19async function getCategoryData(category) {
20 const categoryData = await fetch(`https://api.example.com/categories/${category}`);
21 return categoryData.json();
22}
23
24async function getProducts(category) {
25 const products = await fetch(`https://api.example.com/products?category=${category}`);
26 return products.json();
27}
28
29export default async function CategoryPage({ params }) {
30 // Fetch data in parallel
31 const [categoryData, products] = await Promise.all([
32 getCategoryData((await params).category),
33 getProducts((await params).category),
34 ]);
35
36 return (
37 <div className="grid grid-cols-4 gap-4">
38 {/* Statically rendered category header */}
39 <CategoryHeader data={categoryData} />
40
41 {/* Interactive filter rendered on the client side */}
42 <div className="col-span-1">
43 <FilterSidebar initialProducts={products} />
44 </div>
45
46 {/* Statically rendered product list */}
47 <div className="col-span-3">
48 <ProductList products={products} />
49 </div>
50 </div>
51 );
52}In this example:
generateStaticParams)FilterSidebar component is a Client Component, enabling interactive client-side filteringCategoryHeader and ProductList components are Server Components rendered staticallyThe choice of rendering strategy depends on the characteristics of the given part of the application:
Static pages (documentation, landing pages, informational pages):
Dynamic content that doesn't require the latest data (blog, product catalog):
Content requiring the latest data (stock prices, news, sports results):
cache: 'no-store')Personalized content and interactive UI elements (dashboards, forms):
'use client')Hybrid approach (e.g., e-commerce):
In the App Router, we can also specify which dynamic paths should be generated statically during the application build:
1// app/blog/[slug]/page.tsx
2export async function generateStaticParams() {
3 const posts = await fetch('https://api.example.com/posts').then(res => res.json());
4
5 return posts.map((post) => ({
6 slug: post.slug,
7 }));
8}For routes not covered by
generateStaticParams:dynamicParams = false in the segment options, Next.js will return a 404dynamicParams = true (default) or is not set, Next.js will generate the page on demand (SSR)If we have many dynamic subpages (e.g., thousands of products in e-commerce), we can optimize the build process by:
1export async function generateStaticParams() {
2 // Fetch only the most popular products
3 const popularProducts = await fetch('https://api.example.com/products/popular').then(res => res.json());
4
5 return popularProducts.map((product) => ({
6 id: product.id,
7 }));
8}1// app/api/revalidate/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { revalidatePath } from 'next/cache';
4
5export async function POST(request: NextRequest) {
6 const { path, secret } = await request.json();
7
8 // Checking the secret key for security
9 if (secret !== process.env.REVALIDATION_SECRET) {
10 return NextResponse.json({ message: 'Invalid secret' }, { status: 401 });
11 }
12
13 if (!path) {
14 return NextResponse.json({ message: 'Path is required' }, { status: 400 });
15 }
16
17 try {
18 // Revalidating the specified path
19 revalidatePath(path);
20 return NextResponse.json({ revalidated: true, message: `Revalidated ${path}` });
21 } catch (err) {
22 return NextResponse.json({ message: 'Error revalidating' }, { status: 500 });
23 }
24}| Feature | SSG | ISR | SSR | CSR | |---------|-----|-----|-----|-----| | Performance | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | | SEO | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | | Data freshness | ⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | | Server load | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | | Interactivity | ⭐⭐ | ⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ |
The App Router in Next.js offers advanced capabilities for mixing different rendering strategies at the component level, which enables creating efficient and flexible applications. The key to success is choosing the right strategy for each part of the application, considering performance, SEO, and data freshness requirements.
In the next module, we will discuss in more detail the data revalidation mechanisms in Next.js App Router, which are crucial for implementing ISR and ensuring content freshness.