Skip to main content

Next.js Data Fetching Overview

Data fetching is a crucial aspect of building modern web applications. Next.js provides several powerful methods to fetch and render data, each suited for different use cases. This guide will help you understand the various data fetching strategies available in Next.js and when to use each one.

Introduction

Next.js has revolutionized how we approach data fetching in React applications by offering multiple strategies that optimize for performance, developer experience, and SEO. Whether you're building a blog, e-commerce site, or dashboard, understanding these data fetching methods is essential for creating efficient and responsive applications.

Core Data Fetching Methods in Next.js

Next.js provides three primary methods for fetching data:

  1. Server-Side Rendering (SSR)
  2. Static Site Generation (SSG)
  3. Client-Side Rendering (CSR)

Additionally, Next.js offers more specialized methods:

  1. Incremental Static Regeneration (ISR)
  2. Server Components (in Next.js 13+ with App Router)

Let's explore each of these in detail.

Server-Side Rendering (SSR)

Server-Side Rendering generates HTML for a page on each request. This approach is ideal for pages with frequently changing data or that require user-specific information.

How SSR Works in Next.js

In the Pages Router, SSR is implemented using the getServerSideProps function:

jsx
// pages/ssr-example.js
export default function SSRPage({ data }) {
return (
<div>
<h1>Server-side Rendered Page</h1>
<p>Data fetched at request time: {data.timestamp}</p>
<ul>
{data.items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}

export async function getServerSideProps() {
// This code runs on the server for every request
const res = await fetch('https://api.example.com/data');
const data = await res.json();

return {
props: {
data: {
items: data.items,
timestamp: new Date().toISOString(),
}
}
};
}

In the App Router (Next.js 13+), you can use Server Components which are server-side rendered by default:

jsx
// app/ssr-example/page.js
async function getData() {
const res = await fetch('https://api.example.com/data', { cache: 'no-store' });
return res.json();
}

export default async function SSRPage() {
const data = await getData();

return (
<div>
<h1>Server-side Rendered Page</h1>
<p>Data fetched at request time: {new Date().toISOString()}</p>
<ul>
{data.items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}

Use Cases for SSR

  • User-specific content (dashboards, accounts)
  • Pages with frequently updating data
  • Pages requiring access to request data (headers, cookies)
  • SEO-important pages with dynamic content

Static Site Generation (SSG)

Static Site Generation pre-renders pages at build time, making them extremely fast to serve and great for SEO.

How SSG Works in Next.js

In the Pages Router, SSG is implemented using getStaticProps:

jsx
// pages/ssg-example.js
export default function SSGPage({ data }) {
return (
<div>
<h1>Static Generated Page</h1>
<p>This page was generated at build time.</p>
<p>Last build: {data.buildTime}</p>
<ul>
{data.products.map((product) => (
<li key={product.id}>{product.name} - ${product.price}</li>
))}
</ul>
</div>
);
}

export async function getStaticProps() {
// This code runs at build time
const res = await fetch('https://api.example.com/products');
const products = await res.json();

return {
props: {
data: {
products,
buildTime: new Date().toISOString(),
}
}
};
}

In the App Router, static rendering is the default:

jsx
// app/ssg-example/page.js
async function getProducts() {
const res = await fetch('https://api.example.com/products');
return res.json();
}

export default async function SSGPage() {
const products = await getProducts();

return (
<div>
<h1>Static Generated Page</h1>
<p>This page was generated at build time.</p>
<p>Last build: {new Date().toISOString()}</p>
<ul>
{products.map((product) => (
<li key={product.id}>{product.name} - ${product.price}</li>
))}
</ul>
</div>
);
}

Use Cases for SSG

  • Marketing pages
  • Blog posts
  • Product listings
  • Documentation sites
  • Any content that doesn't change frequently

Static Site Generation with Dynamic Routes

For pages with paths that depend on external data, Next.js uses getStaticPaths (Pages Router) or generateStaticParams (App Router) along with getStaticProps.

Pages Router Example

jsx
// pages/products/[id].js
export default function Product({ product }) {
return (
<div>
<h1>{product.name}</h1>
<p>Price: ${product.price}</p>
<p>Description: {product.description}</p>
</div>
);
}

export async function getStaticPaths() {
// Fetch list of all product IDs
const res = await fetch('https://api.example.com/products');
const products = await res.json();

// Generate paths for each product
const paths = products.map(product => ({
params: { id: product.id.toString() }
}));

return {
paths,
fallback: 'blocking' // or false or true
};
}

export async function getStaticProps({ params }) {
// Fetch data for a specific product
const res = await fetch(`https://api.example.com/products/${params.id}`);
const product = await res.json();

return {
props: {
product
},
revalidate: 60 // Optional: regenerate page after 60 seconds if requested
};
}

App Router Example

jsx
// app/products/[id]/page.js
export async function generateStaticParams() {
const products = await fetch('https://api.example.com/products').then(res => res.json());

return products.map(product => ({
id: product.id.toString(),
}));
}

async function getProduct(id) {
const product = await fetch(`https://api.example.com/products/${id}`).then(res => res.json());
return product;
}

export default async function ProductPage({ params }) {
const product = await getProduct(params.id);

return (
<div>
<h1>{product.name}</h1>
<p>Price: ${product.price}</p>
<p>Description: {product.description}</p>
</div>
);
}

Incremental Static Regeneration (ISR)

ISR enables you to update static pages after they've been built without needing to rebuild the entire site. This gives you the benefits of static generation with the ability to update content.

Pages Router Example

jsx
// pages/products/[id].js with ISR
export async function getStaticProps({ params }) {
const res = await fetch(`https://api.example.com/products/${params.id}`);
const product = await res.json();

return {
props: {
product
},
// Re-generate page at most once every 60 seconds
revalidate: 60
};
}

App Router Example

jsx
// app/products/[id]/page.js with ISR
async function getProduct(id) {
const product = await fetch(`https://api.example.com/products/${id}`, {
next: { revalidate: 60 } // Revalidate this data every 60 seconds
}).then(res => res.json());

return product;
}

Client-Side Data Fetching

Some data doesn't need to be pre-rendered and can be fetched directly from the client. This is useful for user-specific or frequently updated data.

SWR Example

SWR is a React hooks library for data fetching created by the Next.js team.

jsx
// Using SWR for client-side data fetching
import useSWR from 'swr';

const fetcher = (...args) => fetch(...args).then(res => res.json());

export default function Dashboard() {
const { data, error, isLoading } = useSWR('/api/user/dashboard', fetcher, {
refreshInterval: 5000 // Refresh every 5 seconds
});

if (error) return <div>Failed to load</div>;
if (isLoading) return <div>Loading...</div>;

return (
<div>
<h1>Welcome, {data.user.name}</h1>
<p>Account Balance: ${data.account.balance}</p>
<h2>Recent Transactions</h2>
<ul>
{data.transactions.map(tx => (
<li key={tx.id}>{tx.description} - ${tx.amount}</li>
))}
</ul>
</div>
);
}

Choosing the Right Data Fetching Method

Here's a quick reference guide for choosing the appropriate data fetching strategy:

MethodUse CaseProsCons
SSGStatic content that's known at build timeFastest performance, reduced server loadNot suitable for frequently changing data
ISRContent that changes occasionallyBalances performance with freshnessPotential for stale data within revalidation window
SSRHighly dynamic or personalized contentAlways fresh data, access to request contextHigher server load, slower TTFB
CSRNon-SEO critical, highly interactive UIsReduces initial page load, real-time updatesPoor SEO, potentially slower perceived performance

Real-World Application Example

Let's build a simple blog with different data fetching methods:

jsx
// app/page.js
async function getFeaturedPosts() {
const res = await fetch('https://api.myblog.com/featured-posts');
return res.json();
}

export default async function HomePage() {
const featuredPosts = await getFeaturedPosts();

return (
<div>
<h1>My Next.js Blog</h1>
<section>
<h2>Featured Posts</h2>
<div className="post-grid">
{featuredPosts.map(post => (
<div key={post.id} className="post-card">
<img src={post.thumbnail} alt={post.title} />
<h3>{post.title}</h3>
<p>{post.excerpt}</p>
<a href={`/posts/${post.slug}`}>Read more</a>
</div>
))}
</div>
</section>
</div>
);
}

Blog Post Page (ISR)

jsx
// app/posts/[slug]/page.js
export async function generateStaticParams() {
const posts = await fetch('https://api.myblog.com/posts').then(res => res.json());

return posts.map(post => ({
slug: post.slug,
}));
}

async function getPost(slug) {
const post = await fetch(`https://api.myblog.com/posts/${slug}`, {
next: { revalidate: 3600 } // Revalidate every hour
}).then(res => res.json());

return post;
}

export default async function PostPage({ params }) {
const post = await getPost(params.slug);

return (
<article>
<h1>{post.title}</h1>
<div className="post-meta">
<span>By {post.author}</span>
<span>Published on {new Date(post.publishedAt).toLocaleDateString()}</span>
</div>
<img className="post-cover" src={post.coverImage} alt={post.title} />
<div className="post-content" dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}

Comments Section (Client-side)

jsx
'use client';

import { useState, useEffect } from 'react';
import { useParams } from 'next/navigation';

export default function CommentsSection() {
const { slug } = useParams();
const [comments, setComments] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [newComment, setNewComment] = useState('');

useEffect(() => {
async function fetchComments() {
try {
const res = await fetch(`/api/posts/${slug}/comments`);
const data = await res.json();
setComments(data);
setIsLoading(false);
} catch (error) {
console.error('Failed to load comments:', error);
setIsLoading(false);
}
}

fetchComments();

// Set up polling for new comments
const interval = setInterval(fetchComments, 30000);
return () => clearInterval(interval);
}, [slug]);

const handleSubmit = async (e) => {
e.preventDefault();

try {
const res = await fetch(`/api/posts/${slug}/comments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: newComment })
});

if (res.ok) {
const newCommentData = await res.json();
setComments([...comments, newCommentData]);
setNewComment('');
}
} catch (error) {
console.error('Failed to post comment:', error);
}
};

if (isLoading) return <div>Loading comments...</div>;

return (
<section className="comments-section">
<h3>Comments ({comments.length})</h3>
<ul className="comments-list">
{comments.map(comment => (
<li key={comment.id} className="comment">
<div className="comment-header">
<strong>{comment.author}</strong>
<span>{new Date(comment.createdAt).toLocaleString()}</span>
</div>
<p>{comment.content}</p>
</li>
))}
</ul>

<form onSubmit={handleSubmit} className="comment-form">
<textarea
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
placeholder="Leave a comment..."
required
/>
<button type="submit">Post Comment</button>
</form>
</section>
);
}

Dashboard (SSR)

jsx
// app/dashboard/page.js
import { cookies } from 'next/headers';

async function getUserDashboard(token) {
const res = await fetch('https://api.myblog.com/user/dashboard', {
headers: { 'Authorization': `Bearer ${token}` },
cache: 'no-store' // Ensure fresh data on each request
});
return res.json();
}

export default async function DashboardPage() {
const cookieStore = cookies();
const token = cookieStore.get('auth_token')?.value;

if (!token) {
redirect('/login');
}

const dashboard = await getUserDashboard(token);

return (
<div className="dashboard">
<h1>Welcome, {dashboard.user.name}</h1>

<section>
<h2>Your Posts</h2>
<table>
<thead>
<tr>
<th>Title</th>
<th>Published</th>
<th>Views</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{dashboard.posts.map(post => (
<tr key={post.id}>
<td>{post.title}</td>
<td>{post.published ? 'Yes' : 'Draft'}</td>
<td>{post.views}</td>
<td>
<a href={`/editor/${post.id}`}>Edit</a>
<button>Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</section>

<section>
<h2>Analytics</h2>
<div className="stats-grid">
<div className="stat-card">
<h3>Total Views</h3>
<p className="stat-value">{dashboard.stats.totalViews}</p>
</div>
<div className="stat-card">
<h3>Total Comments</h3>
<p className="stat-value">{dashboard.stats.totalComments}</p>
</div>
<div className="stat-card">
<h3>New Subscribers</h3>
<p className="stat-value">{dashboard.stats.newSubscribers}</p>
</div>
</div>
</section>
</div>
);
}

Summary

Next.js provides a comprehensive toolkit for data fetching, allowing developers to choose the most appropriate method for each use case:

  • Server-Side Rendering (SSR) guarantees fresh data on every request, making it ideal for personalized or highly dynamic content.
  • Static Site Generation (SSG) pre-renders pages at build time for optimal performance and SEO.
  • Incremental Static Regeneration (ISR) combines the benefits of static generation with the ability to update content after deployment.
  • Client-Side Rendering is perfect for interactive, non-SEO critical components that need real-time updates.

By combining these strategies in your Next.js application, you can create highly performant, SEO-friendly websites that provide excellent user experiences.

Additional Resources

Exercises

  1. Create a simple blog homepage using SSG that displays a list of posts.
  2. Add individual blog post pages using ISR with a revalidation period of 1 hour.
  3. Implement a comments section for blog posts using client-side rendering.
  4. Create a user dashboard page using SSR that displays personalized content.
  5. Use the App Router's streaming capabilities to improve loading UX on a complex page.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)