Skip to content

Next.js Data Fetching

Next.js provides multiple data fetching methods, supporting different rendering strategies and use cases. This chapter details the various data fetching approaches and their best practices.

Data Fetching Overview

Next.js supports the following main data fetching methods:

  • getStaticProps - Fetch data during static generation
  • getServerSideProps - Fetch data during server-side rendering
  • getStaticPaths - Static generation for dynamic routes
  • Client-side Data Fetching - Using useEffect or SWR/React Query
  • App Router Data Fetching - The new approach in Next.js 13+

getStaticProps - Static Generation

getStaticProps runs at build time and is suitable for pages where data doesn't change frequently.

Basic Usage

javascript
// pages/blog.js
export default function Blog({ posts }) {
  return (
    <div>
      <h1>Blog Posts</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  )
}

export async function getStaticProps() {
  // Fetch data from API or file system
  const res = await fetch('https://api.example.com/posts')
  const posts = await res.json()

  return {
    props: {
      posts,
    },
    // Optional: revalidation time (seconds)
    revalidate: 60, // ISR - Incremental Static Regeneration
  }
}

Advanced Configuration

javascript
export async function getStaticProps(context) {
  const { params, preview, previewData, locale } = context

  try {
    const posts = await fetchPosts()
    
    return {
      props: {
        posts,
      },
      // Revalidation settings
      revalidate: 3600, // 1 hour
      // 404 handling
      notFound: posts.length === 0,
      // Redirect
      redirect: posts.length === 0 ? {
        destination: '/no-posts',
        permanent: false,
      } : undefined,
    }
  } catch (error) {
    return {
      notFound: true,
    }
  }
}

getServerSideProps - Server-Side Rendering

getServerSideProps runs on every request and is suitable for pages that need real-time data.

Basic Usage

javascript
// pages/dashboard.js
export default function Dashboard({ user, stats }) {
  return (
    <div>
      <h1>Welcome, {user.name}</h1>
      <div>
        <p>Today's visits: {stats.todayVisits}</p>
        <p>Total users: {stats.totalUsers}</p>
      </div>
    </div>
  )
}

export async function getServerSideProps(context) {
  const { req, res, query, params } = context
  
  // Get user info (based on cookie or session)
  const user = await getUserFromRequest(req)
  
  if (!user) {
    return {
      redirect: {
        destination: '/login',
        permanent: false,
      },
    }
  }

  // Fetch real-time statistics
  const stats = await fetchUserStats(user.id)

  return {
    props: {
      user,
      stats,
    },
  }
}

Error Handling and Caching

javascript
export async function getServerSideProps(context) {
  const { res } = context
  
  try {
    const data = await fetchData()
    
    // Set cache headers
    res.setHeader(
      'Cache-Control',
      'public, s-maxage=10, stale-while-revalidate=59'
    )
    
    return {
      props: { data },
    }
  } catch (error) {
    console.error('Data fetch failed:', error)
    
    return {
      props: {
        error: 'Data loading failed',
      },
    }
  }
}

getStaticPaths - Dynamic Routes

Used for static generation of dynamic routes, defining which paths need to be pre-rendered.

Basic Usage

javascript
// pages/posts/[id].js
export default function Post({ post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

export async function getStaticPaths() {
  // Get all post IDs
  const posts = await fetchAllPosts()
  
  const paths = posts.map(post => ({
    params: { id: post.id.toString() }
  }))

  return {
    paths,
    fallback: false, // 404 for non-existent paths
  }
}

export async function getStaticProps({ params }) {
  const post = await fetchPost(params.id)
  
  if (!post) {
    return {
      notFound: true,
    }
  }

  return {
    props: { post },
    revalidate: 3600,
  }
}

Fallback Strategies

javascript
export async function getStaticPaths() {
  // Only pre-render the most popular posts
  const popularPosts = await fetchPopularPosts()
  
  const paths = popularPosts.map(post => ({
    params: { id: post.id.toString() }
  }))

  return {
    paths,
    fallback: 'blocking', // Other paths generated on first request
  }
}

// Handling fallback state
export default function Post({ post }) {
  const router = useRouter()
  
  if (router.isFallback) {
    return <div>Loading...</div>
  }
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

Client-Side Data Fetching

Using useEffect

javascript
import { useState, useEffect } from 'react'

export default function Profile() {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    async function fetchUser() {
      try {
        const response = await fetch('/api/user')
        if (!response.ok) {
          throw new Error('Failed to fetch user info')
        }
        const userData = await response.json()
        setUser(userData)
      } catch (err) {
        setError(err.message)
      } finally {
        setLoading(false)
      }
    }

    fetchUser()
  }, [])

  if (loading) return <div>Loading...</div>
  if (error) return <div>Error: {error}</div>
  if (!user) return <div>User not found</div>

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  )
}

Using SWR

SWR is a React Hook library for data fetching that provides caching, revalidation, and more.

javascript
import useSWR from 'swr'

const fetcher = (url) => fetch(url).then(res => res.json())

export default function Profile() {
  const { data: user, error, isLoading } = useSWR('/api/user', fetcher)

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

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  )
}

Advanced SWR Usage

javascript
import useSWR, { mutate } from 'swr'

export default function TodoList() {
  const { data: todos, error } = useSWR('/api/todos', fetcher, {
    refreshInterval: 1000, // Refresh every second
    revalidateOnFocus: false, // Don't revalidate on focus
    dedupingInterval: 2000, // Dedupe requests within 2 seconds
  })

  const addTodo = async (text) => {
    // Optimistic update
    const newTodo = { id: Date.now(), text, completed: false }
    mutate('/api/todos', [...todos, newTodo], false)
    
    try {
      await fetch('/api/todos', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text }),
      })
      // Revalidate data
      mutate('/api/todos')
    } catch (error) {
      // Rollback optimistic update
      mutate('/api/todos')
    }
  }

  return (
    <div>
      {todos?.map(todo => (
        <div key={todo.id}>{todo.text}</div>
      ))}
    </div>
  )
}

App Router Data Fetching (Next.js 13+)

Next.js 13 introduced the new App Router, providing a more concise data fetching approach.

Server Component Data Fetching

javascript
// app/posts/page.js
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 } // Cache for 1 hour
  })
  
  if (!res.ok) {
    throw new Error('Failed to fetch posts')
  }
  
  return res.json()
}

export default async function PostsPage() {
  const posts = await getPosts()
  
  return (
    <div>
      <h1>Post List</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  )
}

Parallel Data Fetching

javascript
// app/dashboard/page.js
async function getUser() {
  const res = await fetch('https://api.example.com/user')
  return res.json()
}

async function getStats() {
  const res = await fetch('https://api.example.com/stats')
  return res.json()
}

export default async function Dashboard() {
  // Fetch data in parallel
  const [user, stats] = await Promise.all([
    getUser(),
    getStats()
  ])
  
  return (
    <div>
      <h1>Welcome, {user.name}</h1>
      <div>Visits: {stats.visits}</div>
    </div>
  )
}

Streaming and Suspense

javascript
// app/posts/page.js
import { Suspense } from 'react'

async function PostList() {
  const posts = await getPosts()
  
  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
        </article>
      ))}
    </div>
  )
}

export default function PostsPage() {
  return (
    <div>
      <h1>Post List</h1>
      <Suspense fallback={<div>Loading posts...</div>}>
        <PostList />
      </Suspense>
    </div>
  )
}

Data Fetching Best Practices

1. Choose the Right Data Fetching Method

javascript
// Static content - use getStaticProps
export async function getStaticProps() {
  const posts = await fetchBlogPosts()
  return { props: { posts }, revalidate: 3600 }
}

// User-specific content - use getServerSideProps
export async function getServerSideProps(context) {
  const user = await getUserFromSession(context.req)
  return { props: { user } }
}

// Real-time content - use client-side fetching
function LiveData() {
  const { data } = useSWR('/api/live-data', fetcher, {
    refreshInterval: 1000
  })
  return <div>{data?.value}</div>
}

2. Error Handling

javascript
// Unified error handling
async function fetchWithErrorHandling(url) {
  try {
    const response = await fetch(url)
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`)
    }
    
    return await response.json()
  } catch (error) {
    console.error('Data fetch failed:', error)
    throw error
  }
}

// Using in component
export async function getStaticProps() {
  try {
    const data = await fetchWithErrorHandling('/api/data')
    return { props: { data } }
  } catch (error) {
    return {
      props: { error: error.message },
      revalidate: 60, // Retry after 1 minute
    }
  }
}

3. Performance Optimization

javascript
// Data prefetching
import { useRouter } from 'next/router'
import Link from 'next/link'

function PostLink({ post }) {
  const router = useRouter()
  
  return (
    <Link
      href={`/posts/${post.id}`}
      onMouseEnter={() => {
        // Prefetch page data
        router.prefetch(`/posts/${post.id}`)
      }}
    >
      {post.title}
    </Link>
  )
}

// Conditional data fetching
export async function getServerSideProps(context) {
  const { user } = context.req
  
  // Only fetch data for logged-in users
  if (!user) {
    return {
      redirect: { destination: '/login', permanent: false }
    }
  }
  
  const userData = await fetchUserData(user.id)
  return { props: { userData } }
}

4. Caching Strategies

javascript
// ISR caching strategy
export async function getStaticProps() {
  const data = await fetchData()
  
  return {
    props: { data },
    revalidate: 60, // Regenerate after 60 seconds
  }
}

// Client-side caching
const { data } = useSWR('/api/data', fetcher, {
  dedupingInterval: 2000, // Dedupe within 2 seconds
  focusThrottleInterval: 5000, // Limit focus revalidation within 5 seconds
  errorRetryInterval: 5000, // Error retry interval
})

Summary

Next.js provides flexible data fetching solutions:

  • getStaticProps: Suitable for static content, fetches data at build time
  • getServerSideProps: Suitable for dynamic content, fetches data on every request
  • getStaticPaths: Used for static generation of dynamic routes
  • Client-side Fetching: Suitable for user interactions and real-time data
  • App Router: The new data fetching approach in Next.js 13+

Choosing the right method depends on your specific needs: data update frequency, SEO requirements, performance considerations, etc. Proper use of these methods can build high-performance applications with excellent user experience.

Content is for learning and research only.