Next.js Complete Guide Part 3: Data Fetching - SSG, SSR, and ISR Explained

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.

By Next.js Expert
13 min read
2455 words

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 paths
  • fallback: 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

  1. Build a Blog with SSG

    • Create a blog using getStaticProps and getStaticPaths
    • Read markdown files from the file system
    • Implement pagination and categories
  2. Create a Dashboard with SSR

    • Build a user dashboard with getServerSideProps
    • Implement authentication checks
    • Display real-time data
  3. E-commerce Product Pages with ISR

    • Create product pages that update periodically
    • Implement on-demand revalidation
    • Handle inventory updates
  4. 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.

Share this article: