Skip to main content

Chapter 7: Data Fetching and Server-Side Rendering (SSR)

What is Data Fetching in Next.js?

Data fetching in Next.js refers to the various methods of loading data into your application, including server-side rendering (SSR), static site generation (SSG), and client-side rendering (CSR). Next.js provides multiple data fetching methods to optimize performance and user experience.

Data Fetching Methods:

  • Server-Side Rendering (SSR): Render pages on the server for each request
  • Static Site Generation (SSG): Pre-render pages at build time
  • Incremental Static Regeneration (ISR): Update static content without rebuilding
  • Client-Side Rendering (CSR): Fetch data on the client side
  • API Routes: Build backend functionality within Next.js

Why Use Different Data Fetching Methods?

Server-Side Rendering (SSR):

  • SEO Benefits: Search engines can crawl fully rendered content
  • Performance: Faster initial page load
  • Dynamic Content: Perfect for user-specific or frequently changing data
  • Social Sharing: Proper meta tags for social media previews

Static Site Generation (SSG):

  • Performance: Fastest possible loading times
  • CDN Friendly: Can be served from CDN globally
  • Cost Effective: Reduced server load
  • Reliability: No server dependencies at runtime

Incremental Static Regeneration (ISR):

  • Best of Both Worlds: Static performance with dynamic updates
  • Scalability: Handle traffic spikes without server overload
  • Flexibility: Update content without full rebuilds

How to Implement Data Fetching Methods

1. Server-Side Rendering (SSR) with getServerSideProps

Basic SSR Implementation:

// pages/blog/[slug].js
import { useRouter } from 'next/router'

export default function BlogPost({ post, error }) {
const router = useRouter()

if (router.isFallback) {
return <div>Loading...</div>
}

if (error) {
return <div>Error: {error}</div>
}

return (
<article>
<h1>{post.title}</h1>
<p>Published: {post.publishedAt}</p>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}

export async function getServerSideProps(context) {
const { slug } = context.params
const { req, res } = context

try {
// Fetch data from API or database
const response = await fetch(`https://api.example.com/posts/${slug}`)

if (!response.ok) {
throw new Error('Failed to fetch post')
}

const post = await response.json()

// Set cache headers
res.setHeader(
'Cache-Control',
'public, s-maxage=10, stale-while-revalidate=59'
)

return {
props: {
post,
},
}
} catch (error) {
return {
props: {
error: error.message,
},
}
}
}

SSR with Authentication:

// pages/dashboard.js
import { getSession } from 'next-auth/react'

export default function Dashboard({ user, data }) {
return (
<div>
<h1>Welcome, {user.name}!</h1>
<div>
{data.map(item => (
<div key={item.id}>{item.title}</div>
))}
</div>
</div>
)
}

export async function getServerSideProps(context) {
const session = await getSession(context)

if (!session) {
return {
redirect: {
destination: '/login',
permanent: false,
},
}
}

// Fetch user-specific data
const response = await fetch(`https://api.example.com/users/${session.user.id}/data`, {
headers: {
'Authorization': `Bearer ${session.accessToken}`,
},
})

const data = await response.json()

return {
props: {
user: session.user,
data,
},
}
}

SSR with Query Parameters:

// pages/search.js
export default function Search({ results, query, page }) {
return (
<div>
<h1>Search Results for: {query}</h1>
<div>
{results.map(result => (
<div key={result.id}>{result.title}</div>
))}
</div>
<div>Page: {page}</div>
</div>
)
}

export async function getServerSideProps(context) {
const { query, page = 1 } = context.query

if (!query) {
return {
redirect: {
destination: '/',
permanent: false,
},
}
}

const response = await fetch(
`https://api.example.com/search?q=${encodeURIComponent(query)}&page=${page}`
)
const results = await response.json()

return {
props: {
results: results.items,
query,
page: parseInt(page),
},
}
}

2. Static Site Generation (SSG) with getStaticProps

Basic SSG Implementation:

// pages/blog/index.js
import Link from 'next/link'

export default function Blog({ posts }) {
return (
<div>
<h1>Blog Posts</h1>
<ul>
{posts.map(post => (
<li key={post.id}>
<Link href={`/blog/${post.slug}`}>
<a>{post.title}</a>
</Link>
</li>
))}
</ul>
</div>
)
}

export async function getStaticProps() {
try {
const response = await fetch('https://api.example.com/posts')
const posts = await response.json()

return {
props: {
posts,
},
// Revalidate every 60 seconds
revalidate: 60,
}
} catch (error) {
return {
props: {
posts: [],
},
}
}
}

SSG with Dynamic Routes:

// pages/blog/[slug].js
import { useRouter } from 'next/router'

export default function BlogPost({ post }) {
const router = useRouter()

if (router.isFallback) {
return <div>Loading...</div>
}

return (
<article>
<h1>{post.title}</h1>
<p>Published: {post.publishedAt}</p>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}

export async function getStaticPaths() {
// Fetch all blog post slugs
const response = await fetch('https://api.example.com/posts')
const posts = await response.json()

const paths = posts.map(post => ({
params: { slug: post.slug },
}))

return {
paths,
// Enable fallback for new posts
fallback: true,
}
}

export async function getStaticProps({ params }) {
try {
const response = await fetch(`https://api.example.com/posts/${params.slug}`)
const post = await response.json()

return {
props: {
post,
},
// Revalidate every hour
revalidate: 3600,
}
} catch (error) {
return {
notFound: true,
}
}
}

3. Incremental Static Regeneration (ISR)

ISR Implementation:

// pages/products/[id].js
export default function Product({ product }) {
return (
<div>
<h1>{product.name}</h1>
<p>Price: ${product.price}</p>
<p>Stock: {product.stock}</p>
<img src={product.image} alt={product.name} />
</div>
)
}

export async function getStaticPaths() {
// Pre-generate popular products
const response = await fetch('https://api.example.com/products/popular')
const products = await response.json()

const paths = products.map(product => ({
params: { id: product.id.toString() },
}))

return {
paths,
fallback: 'blocking', // Generate on-demand
}
}

export async function getStaticProps({ params }) {
const response = await fetch(`https://api.example.com/products/${params.id}`)
const product = await response.json()

return {
props: {
product,
},
// Revalidate every 5 minutes
revalidate: 300,
}
}

ISR with On-Demand Revalidation:

// 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.REVALIDATE_SECRET) {
return res.status(401).json({ message: 'Invalid token' })
}

try {
const { path } = req.body

// Revalidate the specific path
await res.revalidate(path)

return res.json({ revalidated: true })
} catch (err) {
return res.status(500).send('Error revalidating')
}
}

4. Client-Side Data Fetching

Using useEffect:

// components/UserProfile.js
import { useState, useEffect } from 'react'

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

useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true)
const response = await fetch(`/api/users/${userId}`)

if (!response.ok) {
throw new Error('Failed to fetch user')
}

const userData = await response.json()
setUser(userData)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}

fetchUser()
}, [userId])

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>
<img src={user.avatar} alt={user.name} />
</div>
)
}

Using SWR for Data Fetching:

npm install swr
// components/UserList.js
import useSWR from 'swr'

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

export default function UserList() {
const { data, error, mutate } = useSWR('/api/users', fetcher, {
refreshInterval: 10000, // Refresh every 10 seconds
revalidateOnFocus: true,
})

if (error) return <div>Failed to load users</div>
if (!data) return <div>Loading...</div>

return (
<div>
<h1>Users</h1>
<ul>
{data.map(user => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
<button onClick={() => mutate()}>Refresh</button>
</div>
)
}

Using React Query:

npm install @tanstack/react-query
// pages/_app.js
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'

export default function MyApp({ Component, pageProps }) {
const [queryClient] = useState(() => new QueryClient())

return (
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
)
}

// components/ProductList.js
import { useQuery } from '@tanstack/react-query'

const fetchProducts = async () => {
const response = await fetch('/api/products')
return response.json()
}

export default function ProductList() {
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
staleTime: 5 * 60 * 1000, // 5 minutes
})

if (isLoading) return <div>Loading products...</div>
if (error) return <div>Error: {error.message}</div>

return (
<div>
<h1>Products</h1>
<button onClick={() => refetch()}>Refresh</button>
<ul>
{data?.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</div>
)
}

Advanced Data Fetching Patterns

1. Parallel Data Fetching

// pages/dashboard.js
export default function Dashboard({ user, posts, notifications }) {
return (
<div>
<h1>Welcome, {user.name}!</h1>
<div>
<h2>Recent Posts</h2>
{posts.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
<div>
<h2>Notifications</h2>
{notifications.map(notification => (
<div key={notification.id}>{notification.message}</div>
))}
</div>
</div>
)
}

export async function getServerSideProps() {
// Fetch data in parallel
const [userResponse, postsResponse, notificationsResponse] = await Promise.all([
fetch('https://api.example.com/user'),
fetch('https://api.example.com/posts'),
fetch('https://api.example.com/notifications'),
])

const [user, posts, notifications] = await Promise.all([
userResponse.json(),
postsResponse.json(),
notificationsResponse.json(),
])

return {
props: {
user,
posts,
notifications,
},
}
}

2. Conditional Data Fetching

// pages/profile.js
export default function Profile({ user, isPublic }) {
return (
<div>
<h1>{user.name}</h1>
{!isPublic && <p>Email: {user.email}</p>}
<p>Bio: {user.bio}</p>
</div>
)
}

export async function getServerSideProps(context) {
const { req } = context
const isPublic = req.headers.host.includes('public')

if (isPublic) {
// Fetch public profile data only
const response = await fetch(`https://api.example.com/public-profile/${context.params.id}`)
const user = await response.json()

return {
props: {
user,
isPublic: true,
},
}
} else {
// Fetch full profile data
const response = await fetch(`https://api.example.com/profile/${context.params.id}`)
const user = await response.json()

return {
props: {
user,
isPublic: false,
},
}
}
}

3. Error Handling and Fallbacks

// pages/blog/[slug].js
export default function BlogPost({ post, error }) {
if (error) {
return (
<div>
<h1>Error Loading Post</h1>
<p>{error}</p>
<a href="/blog">Back to Blog</a>
</div>
)
}

return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}

export async function getServerSideProps(context) {
try {
const response = await fetch(`https://api.example.com/posts/${context.params.slug}`)

if (response.status === 404) {
return {
notFound: true,
}
}

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}

const post = await response.json()

return {
props: {
post,
},
}
} catch (error) {
return {
props: {
error: error.message,
},
}
}
}

Performance Optimization

1. Caching Strategies

// pages/api/posts.js
export default async function handler(req, res) {
// Set cache headers
res.setHeader('Cache-Control', 'public, s-maxage=60, stale-while-revalidate=300')

const posts = await fetchPosts()

res.status(200).json(posts)
}

2. Data Prefetching

// components/PostLink.js
import Link from 'next/link'
import { useRouter } from 'next/router'

export default function PostLink({ post }) {
const router = useRouter()

const handleMouseEnter = () => {
// Prefetch the post page
router.prefetch(`/blog/${post.slug}`)
}

return (
<Link href={`/blog/${post.slug}`}>
<a onMouseEnter={handleMouseEnter}>
{post.title}
</a>
</Link>
)
}

Best Practices

1. Choose the Right Method

// ✅ Use SSG for static content
export async function getStaticProps() {
// Blog posts, product catalogs, documentation
}

// ✅ Use SSR for dynamic content
export async function getServerSideProps() {
// User dashboards, personalized content
}

// ✅ Use ISR for semi-dynamic content
export async function getStaticProps() {
return {
props: { data },
revalidate: 3600, // 1 hour
}
}

2. Error Handling

// ✅ Always handle errors gracefully
export async function getServerSideProps() {
try {
const data = await fetchData()
return { props: { data } }
} catch (error) {
return {
props: { error: error.message },
// or redirect to error page
// redirect: { destination: '/error', permanent: false }
}
}
}

3. Performance Monitoring

// ✅ Add performance monitoring
export async function getServerSideProps(context) {
const start = Date.now()

const data = await fetchData()

const duration = Date.now() - start
console.log(`Data fetching took ${duration}ms`)

return { props: { data } }
}

Common Mistakes to Avoid

1. Mixing Data Fetching Methods

// ❌ Wrong: Using both getStaticProps and getServerSideProps
export async function getStaticProps() { /* ... */ }
export async function getServerSideProps() { /* ... */ }

// ✅ Correct: Use only one method per page
export async function getServerSideProps() { /* ... */ }

2. Not Handling Loading States

// ❌ Wrong: No loading state
export default function Page({ data }) {
return <div>{data.title}</div>
}

// ✅ Correct: Handle loading state
export default function Page({ data }) {
if (!data) return <div>Loading...</div>
return <div>{data.title}</div>
}

3. Inefficient Data Fetching

// ❌ Wrong: Fetching data in useEffect when SSR is better
export default function Page() {
const [data, setData] = useState(null)

useEffect(() => {
fetchData().then(setData)
}, [])

return <div>{data?.title}</div>
}

// ✅ Correct: Use getServerSideProps for initial data
export async function getServerSideProps() {
const data = await fetchData()
return { props: { data } }
}

Summary

Data fetching in Next.js provides powerful tools for building performant applications:

  • SSR for dynamic, user-specific content
  • SSG for static content with optimal performance
  • ISR for the best of both worlds
  • Client-side fetching for interactive features
  • Proper error handling ensures robust applications

Key Takeaways:

  • Choose the right data fetching method for your use case
  • Use SSG for static content, SSR for dynamic content
  • Implement proper error handling and loading states
  • Optimize performance with caching and prefetching
  • Monitor and measure data fetching performance

This tutorial is part of the comprehensive Next.js learning path at syscook.dev. Continue to the next chapter to learn about API routes and backend functionality.

Author: syscook.dev
Last Updated: December 2024