Next.js Complete Guide Part 3: Data Fetching - SSG, SSR, and ISR Explained
Master Next.js data fetching with getStaticProps, getServerSideProps, and getStaticPaths. Learn Static Site Generation, Server-Side Rendering, and Incremental Static Regeneration with real examples.
Welcome to Part 3 of our comprehensive Next.js series! Now that you understand routing, let's dive into one of Next.js's most powerful features: data fetching. We'll explore Static Site Generation (SSG), Server-Side Rendering (SSR), and Incremental Static Regeneration (ISR).
Understanding Next.js Rendering Methods
Next.js offers multiple ways to render pages, each optimized for different use cases:
1. Static Site Generation (SSG) - Pre-rendered at Build Time
- Pages are generated at build time
- Served as static HTML files
- Fastest performance
- Best for content that doesn't change often
2. Server-Side Rendering (SSR) - Pre-rendered on Each Request
- Pages are generated on each request
- Fresh data on every page load
- Good for dynamic content
- Slightly slower than SSG
3. Client-Side Rendering (CSR) - Rendered in the Browser
- Traditional React approach
- Data fetched after page loads
- Good for user-specific content
- SEO limitations
4. Incremental Static Regeneration (ISR) - Best of Both Worlds
- Static generation with periodic updates
- Combines SSG performance with SSR freshness
- Perfect for frequently updated content
Static Site Generation (SSG) with getStaticProps
getStaticProps runs at build time and pre-generates pages with data.
Basic getStaticProps Example
Create pages/blog/index.js:
import Head from 'next/head'
import Link from 'next/link'
export default function Blog({ posts }) {
return (
<div>
<Head>
<title>Blog - My Next.js Site</title>
<meta name="description" content="Read our latest blog posts" />
</Head>
<main>
<h1>Our Blog</h1>
<div className="posts-grid">
{posts.map(post => (
<article key={post.id} className="post-card">
<h2>
<Link href={`/blog/${post.slug}`}>
{post.title}
</Link>
</h2>
<p>{post.excerpt}</p>
<div className="meta">
<time>{post.date}</time>
<span>By {post.author}</span>
</div>
</article>
))}
</div>
</main>
</div>
)
}
// This function runs at build time
export async function getStaticProps() {
// Fetch data from an API, database, or file system
const posts = [
{
id: 1,
title: 'Getting Started with Next.js',
slug: 'getting-started-nextjs',
excerpt: 'Learn the basics of Next.js development.',
date: '2024-01-15',
author: 'John Doe'
},
{
id: 2,
title: 'Advanced Next.js Patterns',
slug: 'advanced-nextjs-patterns',
excerpt: 'Explore advanced patterns and techniques.',
date: '2024-01-20',
author: 'Jane Smith'
}
]
// The return value will be passed to the page component as props
return {
props: {
posts
}
}
}
Fetching Data from External APIs
// pages/products/index.js
export default function Products({ products }) {
return (
<div>
<h1>Our Products</h1>
<div className="products-grid">
{products.map(product => (
<div key={product.id} className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
))}
</div>
</div>
)
}
export async function getStaticProps() {
try {
// Fetch data from external API
const response = await fetch('https://api.example.com/products')
const products = await response.json()
return {
props: {
products
},
// Regenerate the page at most once every hour
revalidate: 3600
}
} catch (error) {
console.error('Error fetching products:', error)
return {
props: {
products: []
}
}
}
}
Reading Data from File System
// lib/posts.js
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
const postsDirectory = path.join(process.cwd(), 'posts')
export function getAllPosts() {
const fileNames = fs.readdirSync(postsDirectory)
const allPostsData = fileNames.map(fileName => {
const slug = fileName.replace(/\.md$/, '')
const fullPath = path.join(postsDirectory, fileName)
const fileContents = fs.readFileSync(fullPath, 'utf8')
const { data, content } = matter(fileContents)
return {
slug,
content,
...data
}
})
return allPostsData.sort((a, b) => {
return new Date(b.date) - new Date(a.date)
})
}
export function getPostBySlug(slug) {
const fullPath = path.join(postsDirectory, `${slug}.md`)
const fileContents = fs.readFileSync(fullPath, 'utf8')
const { data, content } = matter(fileContents)
return {
slug,
content,
...data
}
}
Use in page:
// pages/blog/index.js
import { getAllPosts } from '../../lib/posts'
export default function Blog({ posts }) {
return (
<div>
<h1>Blog Posts</h1>
{posts.map(post => (
<article key={post.slug}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
<time>{post.date}</time>
</article>
))}
</div>
)
}
export async function getStaticProps() {
const posts = getAllPosts()
return {
props: {
posts
}
}
}
Dynamic Routes with getStaticPaths
For dynamic routes, use getStaticPaths to specify which paths to pre-render.
Basic Dynamic Route Example
Create pages/blog/[slug].js:
import Head from 'next/head'
import { useRouter } from 'next/router'
export default function BlogPost({ post }) {
const router = useRouter()
// Show loading state while page is being generated
if (router.isFallback) {
return <div>Loading...</div>
}
return (
<div>
<Head>
<title>{post.title}</title>
<meta name="description" content={post.excerpt} />
</Head>
<article>
<h1>{post.title}</h1>
<div className="meta">
<time>{post.date}</time>
<span>By {post.author}</span>
</div>
<div className="content">
{post.content}
</div>
</article>
</div>
)
}
// Generate paths for all blog posts
export async function getStaticPaths() {
// Get all post slugs
const posts = [
{ slug: 'getting-started-nextjs' },
{ slug: 'advanced-nextjs-patterns' },
{ slug: 'nextjs-deployment-guide' }
]
// Generate paths
const paths = posts.map(post => ({
params: { slug: post.slug }
}))
return {
paths,
fallback: false // Show 404 for non-existent paths
}
}
// Fetch data for each post
export async function getStaticProps({ params }) {
const { slug } = params
// Fetch post data based on slug
const post = {
title: `Blog Post: ${slug}`,
slug,
excerpt: 'This is a sample blog post excerpt.',
content: 'This is the full content of the blog post...',
date: '2024-01-15',
author: 'John Doe'
}
// Return 404 if post not found
if (!post) {
return {
notFound: true
}
}
return {
props: {
post
}
}
}
Advanced getStaticPaths with Fallback
export async function getStaticPaths() {
// Only pre-render the most popular posts
const popularPosts = await getPopularPosts()
const paths = popularPosts.map(post => ({
params: { slug: post.slug }
}))
return {
paths,
fallback: 'blocking' // Generate other pages on-demand
}
}
export async function getStaticProps({ params }) {
const { slug } = params
try {
const post = await getPostBySlug(slug)
return {
props: { post },
revalidate: 86400 // Revalidate once per day
}
} catch (error) {
return {
notFound: true
}
}
}
Fallback Options:
fallback: false- Show 404 for non-pre-rendered pathsfallback: true- Generate pages on-demand (show loading state)fallback: 'blocking'- Generate pages on-demand (wait for generation)
Server-Side Rendering (SSR) with getServerSideProps
getServerSideProps runs on every request, perfect for dynamic content.
Basic SSR Example
Create pages/dashboard.js:
import { useEffect, useState } from 'react'
export default function Dashboard({ user, initialData }) {
const [data, setData] = useState(initialData)
return (
<div>
<h1>Welcome, {user.name}!</h1>
<div className="dashboard-stats">
<div className="stat">
<h3>Total Orders</h3>
<p>{data.totalOrders}</p>
</div>
<div className="stat">
<h3>Revenue</h3>
<p>${data.revenue}</p>
</div>
<div className="stat">
<h3>Active Users</h3>
<p>{data.activeUsers}</p>
</div>
</div>
</div>
)
}
// This function runs on every request
export async function getServerSideProps(context) {
const { req, res, query } = context
// Access cookies, headers, etc.
const cookies = req.headers.cookie
// Simulate user authentication
const user = await authenticateUser(cookies)
if (!user) {
return {
redirect: {
destination: '/login',
permanent: false
}
}
}
// Fetch fresh data on every request
const dashboardData = await fetchDashboardData(user.id)
return {
props: {
user,
initialData: dashboardData
}
}
}
async function authenticateUser(cookies) {
// Simulate authentication logic
return {
id: 1,
name: 'John Doe',
email: 'john@example.com'
}
}
async function fetchDashboardData(userId) {
// Simulate API call
return {
totalOrders: 150,
revenue: 25000,
activeUsers: 1200
}
}
SSR with External API
// pages/news.js
export default function News({ articles }) {
return (
<div>
<h1>Latest News</h1>
<div className="articles">
{articles.map(article => (
<article key={article.id}>
<h2>{article.title}</h2>
<p>{article.summary}</p>
<time>{new Date(article.publishedAt).toLocaleDateString()}</time>
</article>
))}
</div>
</div>
)
}
export async function getServerSideProps() {
try {
const response = await fetch('https://newsapi.org/v2/top-headlines?country=us&apiKey=YOUR_API_KEY')
const data = await response.json()
return {
props: {
articles: data.articles || []
}
}
} catch (error) {
console.error('Error fetching news:', error)
return {
props: {
articles: []
}
}
}
}
Incremental Static Regeneration (ISR)
ISR combines the benefits of SSG and SSR by allowing static pages to be updated after build time.
Basic ISR Example
// pages/products/[id].js
export default function Product({ product, lastUpdated }) {
return (
<div>
<h1>{product.name}</h1>
<p>Price: ${product.price}</p>
<p>In Stock: {product.inStock ? 'Yes' : 'No'}</p>
<small>Last updated: {lastUpdated}</small>
</div>
)
}
export async function getStaticPaths() {
// Pre-render only the most popular products
const popularProducts = await getPopularProducts()
const paths = popularProducts.map(product => ({
params: { id: product.id.toString() }
}))
return {
paths,
fallback: 'blocking'
}
}
export async function getStaticProps({ params }) {
const { id } = params
try {
const product = await fetchProduct(id)
return {
props: {
product,
lastUpdated: new Date().toISOString()
},
// Regenerate the page at most once every 10 seconds
revalidate: 10
}
} catch (error) {
return {
notFound: true
}
}
}
On-Demand ISR (Next.js 12.2+)
// pages/api/revalidate.js
export default async function handler(req, res) {
// Check for secret to confirm this is a valid request
if (req.query.secret !== process.env.REVALIDATION_SECRET) {
return res.status(401).json({ message: 'Invalid token' })
}
try {
// Revalidate specific pages
await res.revalidate('/products')
await res.revalidate('/products/1')
return res.json({ revalidated: true })
} catch (err) {
return res.status(500).send('Error revalidating')
}
}
Trigger revalidation:
curl -X POST "https://yoursite.com/api/revalidate?secret=your-secret-token"
Client-Side Data Fetching
For user-specific or frequently changing data, use client-side fetching.
Using SWR (Recommended)
Install SWR:
npm install swr
Create a custom hook:
// hooks/useUser.js
import useSWR from 'swr'
const fetcher = (url) => fetch(url).then(res => res.json())
export function useUser() {
const { data, error, isLoading } = useSWR('/api/user', fetcher)
return {
user: data,
isLoading,
isError: error
}
}
Use in component:
// pages/profile.js
import { useUser } from '../hooks/useUser'
export default function Profile() {
const { user, isLoading, isError } = useUser()
if (isError) return <div>Failed to load user</div>
if (isLoading) return <div>Loading...</div>
return (
<div>
<h1>Profile</h1>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
</div>
)
}
Using React Query
Install React Query:
npm install @tanstack/react-query
Setup:
// pages/_app.js
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'
export default function App({ Component, pageProps }) {
const [queryClient] = useState(() => new QueryClient())
return (
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
)
}
Use in component:
// pages/posts.js
import { useQuery } from '@tanstack/react-query'
function fetchPosts() {
return fetch('/api/posts').then(res => res.json())
}
export default function Posts() {
const { data: posts, isLoading, error } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts
})
if (isLoading) return <div>Loading posts...</div>
if (error) return <div>Error loading posts</div>
return (
<div>
<h1>Posts</h1>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
</article>
))}
</div>
)
}
Choosing the Right Data Fetching Method
Decision Tree
Is the data user-specific or requires authentication?
├─ Yes → Use getServerSideProps or Client-side fetching
└─ No → Continue
Does the data change frequently?
├─ Yes → Use ISR with short revalidate time or SSR
└─ No → Continue
Do you need the fastest possible loading?
├─ Yes → Use getStaticProps (SSG)
└─ No → Use ISR for balance of speed and freshness
Use Cases by Method
getStaticProps (SSG):
- Blog posts
- Marketing pages
- Documentation
- Product catalogs (stable)
getServerSideProps (SSR):
- User dashboards
- Real-time data
- Personalized content
- Authentication-required pages
ISR:
- E-commerce product pages
- News articles
- Social media feeds
- Content that updates periodically
Client-side:
- User profiles
- Shopping carts
- Real-time chat
- Interactive dashboards
Performance Optimization Tips
1. Optimize getStaticProps
export async function getStaticProps() {
// Fetch only necessary data
const posts = await getPosts({
limit: 10,
fields: ['title', 'slug', 'excerpt', 'date']
})
return {
props: { posts },
revalidate: 3600 // Cache for 1 hour
}
}
2. Use Parallel Data Fetching
export async function getStaticProps() {
// Fetch data in parallel
const [posts, categories, tags] = await Promise.all([
getPosts(),
getCategories(),
getTags()
])
return {
props: {
posts,
categories,
tags
}
}
}
3. Implement Error Boundaries
// components/ErrorBoundary.js
import { Component } from 'react'
class ErrorBoundary extends Component {
constructor(props) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error) {
return { hasError: true }
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error, errorInfo)
}
render() {
if (this.state.hasError) {
return (
<div>
<h2>Something went wrong.</h2>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
)
}
return this.props.children
}
}
export default ErrorBoundary
Common Data Fetching Patterns
1. Loading States
export default function Posts() {
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
async function fetchPosts() {
try {
setLoading(true)
const response = await fetch('/api/posts')
const data = await response.json()
setPosts(data)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
fetchPosts()
}, [])
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
</article>
))}
</div>
)
}
2. Pagination
export default function PostsList({ initialPosts, totalPages }) {
const [posts, setPosts] = useState(initialPosts)
const [currentPage, setCurrentPage] = useState(1)
const [loading, setLoading] = useState(false)
const loadMore = async () => {
if (currentPage >= totalPages) return
setLoading(true)
const response = await fetch(`/api/posts?page=${currentPage + 1}`)
const newPosts = await response.json()
setPosts(prev => [...prev, ...newPosts])
setCurrentPage(prev => prev + 1)
setLoading(false)
}
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
</article>
))}
{currentPage < totalPages && (
<button onClick={loadMore} disabled={loading}>
{loading ? 'Loading...' : 'Load More'}
</button>
)}
</div>
)
}
export async function getStaticProps() {
const posts = await getPosts({ page: 1, limit: 10 })
const totalPages = await getTotalPages()
return {
props: {
initialPosts: posts,
totalPages
}
}
}
What's Next?
Excellent work! You now understand:
- ✅ Static Site Generation (SSG) with getStaticProps
- ✅ Dynamic routes with getStaticPaths
- ✅ Server-Side Rendering (SSR) with getServerSideProps
- ✅ Incremental Static Regeneration (ISR)
- ✅ Client-side data fetching with SWR and React Query
- ✅ Choosing the right data fetching method
- ✅ Performance optimization techniques
In Part 4, we'll explore:
- API Routes and backend functionality
- Database integration
- Authentication and authorization
- Middleware and request handling
- Building full-stack applications
Practice Exercises
-
Build a Blog with SSG
- Create a blog using getStaticProps and getStaticPaths
- Read markdown files from the file system
- Implement pagination and categories
-
Create a Dashboard with SSR
- Build a user dashboard with getServerSideProps
- Implement authentication checks
- Display real-time data
-
E-commerce Product Pages with ISR
- Create product pages that update periodically
- Implement on-demand revalidation
- Handle inventory updates
-
Client-side Data Fetching
- Build a search interface with SWR
- Implement infinite scrolling
- Add error handling and loading states
Ready for Part 4? Next, we'll build full-stack applications with API routes and learn how to create complete web applications with Next.js!
Continue following our comprehensive Next.js series to become a full-stack Next.js developer.