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
// 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
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
// 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
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
// 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
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
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.
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
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
// 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
// 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
// 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
// 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
// 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
// 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
// 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.