React & Next.js Topic
Next.js Data Fetching & Caching
Next.js Data Fetching & Caching refers to how Next.js retrieves data (from APIs, databases, etc.) and how it stores/reuses that data to make apps faster, scalable, and more efficient.
It’s a core feature that separates Next.js from plain React.
Where Data Fetching Happens
Next.js can fetch data in two places:
Server Components (default in App Router)
-
Runs on the server
-
Data is never exposed to the browser
-
Great for secure data (DB calls, secrets)
-
Better performance (no client JS required)
Client Components
-
Runs in the browser
-
Used for interactive features (forms, buttons, inputs)
-
Typically fetches data using
useEffector libraries like SWR/React Query
Rule of thumb:
Use server components first, client components only when necessary.
1. Data Fetching Methods
Static Site Generation (SSG)
jsx
// pages/products/[id].js
export default function Product({ product }) {
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
</div>
);
}
// getStaticPaths - defines which paths to pre-render
export async function getStaticPaths() {
// Fetch product IDs from API
const products = await fetch('https://api.example.com/products').then(res => res.json());
const paths = products.map(product => ({
params: { id: product.id.toString() }
}));
return {
paths,
fallback: false // or true or 'blocking'
};
}
// getStaticProps - fetches data at build time
export async function getStaticProps({ params }) {
const product = await fetch(`https://api.example.com/products/${params.id}`).then(res => res.json());
return {
props: {
product
},
// ISR: Revalidate every 60 seconds
revalidate: 60
};
}
Server-Side Rendering (SSR)
jsx
// pages/profile.js
export default function Profile({ user, timestamp }) {
return (
<div>
<h1>{user.name}</h1>
<p>Rendered at: {timestamp}</p>
</div>
);
}
// getServerSideProps - runs on every request
export async function getServerSideProps(context) {
const { req, res, params, query } = context;
// Access cookies, headers, etc.
const token = req.cookies.token;
// Fetch user-specific data
const user = await fetch('https://api.example.com/user', {
headers: {
Authorization: `Bearer ${token}`
}
}).then(res => res.json());
return {
props: {
user,
timestamp: new Date().toISOString()
}
};
}
Client-Side Fetching
jsx
import { useState, useEffect } from 'react';
import useSWR from 'swr';
// Using native fetch
function ClientSideComponent() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchData() {
try {
const response = await fetch('/api/users');
const users = await response.json();
setData(users);
} catch (error) {
console.error('Failed to fetch:', error);
} finally {
setLoading(false);
}
}
fetchData();
}, []);
if (loading) return <div>Loading...</div>;
return <div>{/* Render data */}</div>;
}
// Using SWR for better caching and revalidation
function SWRComponent() {
const { data, error, isLoading, mutate } = useSWR(
'/api/users',
(url) => fetch(url).then(res => res.json()),
{
revalidateOnFocus: true,
revalidateOnReconnect: true,
refreshInterval: 30000, // 30 seconds
}
);
if (error) return <div>Failed to load</div>;
if (isLoading) return <div>Loading...</div>;
return (
<div>
{data.map(user => (
<div key={user.id}>{user.name}</div>
))}
<button onClick={() => mutate()}>Refresh</button>
</div>
);
}
2. Fetch with Caching Options
Native Fetch with Next.js Caching
jsx
// lib/api.js
export async function fetchWithCache(url, options = {}) {
const { cache = 'force-cache', revalidate, tags } = options;
const response = await fetch(url, {
...options,
next: {
revalidate, // Time in seconds
tags, // Array of cache tags for on-demand revalidation
},
cache, // 'force-cache' | 'no-store' | 'no-cache' | 'reload' | 'only-if-cached'
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
// Usage in components
export async function getStaticProps() {
// Cache for 1 hour (3600 seconds)
const products = await fetchWithCache('https://api.example.com/products', {
revalidate: 3600,
tags: ['products']
});
// Don't cache - fresh data on every request
const userSession = await fetchWithCache('https://api.example.com/session', {
cache: 'no-store'
});
return {
props: {
products,
userSession
}
};
}
Cache Strategies by Use Case
jsx
// pages/blog/[slug].js - SSG with ISR
export async function getStaticProps({ params }) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
next: { revalidate: 3600 } // Revalidate every hour
}).then(res => res.json());
return { props: { post } };
}
// pages/dashboard.js - SSR with no cache
export async function getServerSideProps() {
const sensitiveData = await fetch('https://api.example.com/sensitive-data', {
cache: 'no-store' // Never cache
}).then(res => res.json());
return { props: { sensitiveData } };
}
// pages/analytics.js - Client-side with SWR
function AnalyticsPage() {
const { data } = useSWR('/api/analytics', null, {
refreshInterval: 5000, // Refresh every 5 seconds
dedupingInterval: 2000, // Dedupe requests within 2 seconds
});
return <div>{/* Real-time analytics */}</div>;
}
Advanced Caching Patterns
jsx
// lib/cache.js
export class DataCache {
static async get(key, fetcher, options = {}) {
const { revalidate = 3600, tags = [] } = options;
try {
const data = await fetch(`https://api.example.com/${key}`, {
next: { revalidate, tags }
}).then(res => res.json());
return data;
} catch (error) {
// Fallback to stale data or error handling
console.error('Cache fetch failed:', error);
throw error;
}
}
}
// pages/categories/[category].js
export async function getStaticProps({ params }) {
const [products, filters] = await Promise.all([
DataCache.get(`products?category=${params.category}`, {
revalidate: 1800, // 30 minutes
tags: ['products', `category-${params.category}`]
}),
DataCache.get('filters', {
revalidate: 86400, // 24 hours
tags: ['filters']
})
]);
return {
props: {
products,
filters
},
revalidate: 1800 // ISR for the whole page
};
}
3. Server Actions
Basic Server Actions
jsx
// app/actions/user-actions.js
'use server';
import { revalidateTag } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createUser(formData) {
const user = {
name: formData.get('name'),
email: formData.get('email')
};
// Validate data
if (!user.name || !user.email) {
return {
error: 'Name and email are required'
};
}
try {
// Save to database
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(user)
});
if (!response.ok) {
throw new Error('Failed to create user');
}
// Revalidate cache
revalidateTag('users');
// Redirect to users page
redirect('/users');
} catch (error) {
return {
error: error.message
};
}
}
export async function updateUser(userId, formData) {
'use server';
const updates = {
name: formData.get('name'),
email: formData.get('email')
};
await fetch(`https://api.example.com/users/${userId}`, {
method: 'PATCH',
body: JSON.stringify(updates)
});
revalidateTag('users');
revalidateTag(`user-${userId}`);
}
export async function deleteUser(userId) {
'use server';
await fetch(`https://api.example.com/users/${userId}`, {
method: 'DELETE'
});
revalidateTag('users');
}
Using Server Actions in Components
jsx
// app/components/user-form.js
'use client';
import { useActionState } from 'react';
import { createUser } from '../actions/user-actions';
export function UserForm() {
const [state, formAction, isPending] = useActionState(createUser, null);
return (
<form action={formAction}>
<div>
<label htmlFor="name">Name:</label>
<input type="text" id="name" name="name" required />
</div>
<div>
<label htmlFor="email">Email:</label>
<input type="email" id="email" name="email" required />
</div>
{state?.error && (
<div className="error">{state.error}</div>
)}
<button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create User'}
</button>
</form>
);
}
// app/components/user-list.js
'use client';
import { useOptimistic } from 'react';
import { deleteUser } from '../actions/user-actions';
export function UserList({ users }) {
const [optimisticUsers, setOptimisticUsers] = useOptimistic(
users,
(currentUsers, userIdToDelete) =>
currentUsers.filter(user => user.id !== userIdToDelete)
);
const handleDelete = async (userId) => {
setOptimisticUsers(userId);
await deleteUser(userId);
};
return (
<div>
{optimisticUsers.map(user => (
<div key={user.id} className="user-item">
<span>{user.name} - {user.email}</span>
<button
onClick={() => handleDelete(user.id)}
className="delete-btn"
>
Delete
</button>
</div>
))}
</div>
);
}
Advanced Server Actions with Validation
jsx
// lib/validation.js
import { z } from 'zod';
export const userSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Invalid email address'),
age: z.number().min(0, 'Age must be positive').optional()
});
// app/actions/advanced-actions.js
'use server';
import { revalidatePath } from 'next/cache';
import { userSchema } from '../../lib/validation';
export async function createUserWithValidation(prevState, formData) {
const rawData = {
name: formData.get('name'),
email: formData.get('email'),
age: formData.get('age') ? parseInt(formData.get('age')) : undefined
};
// Validate with Zod
const result = userSchema.safeParse(rawData);
if (!result.success) {
const errors = result.error.flatten().fieldErrors;
return {
error: 'Validation failed',
fieldErrors: errors
};
}
try {
// Save validated data
await saveUser(result.data);
revalidatePath('/users');
return { success: true, message: 'User created successfully' };
} catch (error) {
return {
error: 'Failed to create user',
message: error.message
};
}
}
// app/components/advanced-form.js
'use client';
import { useActionState } from 'react';
import { createUserWithValidation } from '../actions/advanced-actions';
const initialState = {
error: null,
fieldErrors: {},
success: false,
message: ''
};
export function AdvancedUserForm() {
const [state, formAction, isPending] = useActionState(
createUserWithValidation,
initialState
);
return (
<form action={formAction}>
<div>
<label htmlFor="name">Name:</label>
<input
type="text"
id="name"
name="name"
aria-invalid={state.fieldErrors.name ? 'true' : 'false'}
/>
{state.fieldErrors.name && (
<span className="error">{state.fieldErrors.name.join(', ')}</span>
)}
</div>
<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
aria-invalid={state.fieldErrors.email ? 'true' : 'false'}
/>
{state.fieldErrors.email && (
<span className="error">{state.fieldErrors.email.join(', ')}</span>
)}
</div>
{state.error && !state.success && (
<div className="error">{state.error}</div>
)}
{state.success && (
<div className="success">{state.message}</div>
)}
<button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create User'}
</button>
</form>
);
}
4. Static vs Dynamic Rendering
Static Rendering (Default)
jsx
// app/products/page.js - Static by default
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
next: { revalidate: 3600 } // ISR: 1 hour
});
return res.json();
}
export default async function ProductsPage() {
const products = await getProducts(); // This makes the page dynamic!
return (
<div>
<h1>Products</h1>
{products.map(product => (
<div key={product.id}>{product.name}</div>
))}
</div>
);
}
// To keep it truly static, move data fetching to generateStaticParams
// app/products/[id]/page.js
export async function generateStaticParams() {
const products = await fetch('https://api.example.com/products').then(res => res.json());
return products.map(product => ({
id: product.id.toString()
}));
}
export default function ProductPage({ params }) {
// params.id is available from static generation
return <div>Product {params.id}</div>;
}
Dynamic Rendering
jsx
// app/dashboard/page.js - Dynamic rendering
import { cookies, headers } from 'next/headers';
export const dynamic = 'force-dynamic'; // Force dynamic rendering
// or
export const revalidate = 0; // No caching
async function getUserSession() {
const cookieStore = cookies();
const token = cookieStore.get('session-token')?.value;
const headersList = headers();
const userAgent = headersList.get('user-agent');
const user = await fetch('https://api.example.com/user', {
headers: { Authorization: `Bearer ${token}` },
cache: 'no-store' // Don't cache user-specific data
}).then(res => res.json());
return { user, userAgent };
}
export default async function Dashboard() {
const { user, userAgent } = await getUserSession();
return (
<div>
<h1>Welcome, {user.name}</h1>
<p>You're using: {userAgent}</p>
</div>
);
}
Mixed Static and Dynamic
jsx
// app/blog/[slug]/page.js
export async function generateStaticParams() {
// Pre-render popular posts
const popularPosts = await fetch('https://api.example.com/posts/popular', {
next: { tags: ['posts'] }
}).then(res => res.json());
return popularPosts.map(post => ({
slug: post.slug
}));
}
export const dynamicParams = true; // Allow dynamic params not in generateStaticParams
async function getPost(slug) {
const post = await fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 3600 } // Cache for 1 hour
}).then(res => res.json());
return post;
}
export default async function BlogPost({ params }) {
const post = await getPost(params.slug);
if (!post) {
notFound(); // Show 404 for non-existent posts
}
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
export async function generateMetadata({ params }) {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt
};
}
5. Incremental Static Regeneration (ISR)
Basic ISR Implementation
jsx
// pages/products/[id].js
export async function getStaticPaths() {
// Pre-render the top 100 products at build time
const topProducts = await fetch('https://api.example.com/products/top-100')
.then(res => res.json());
const paths = topProducts.map(product => ({
params: { id: product.id.toString() }
}));
return {
paths,
fallback: 'blocking' // Generate new pages on-demand
};
}
export async function getStaticProps({ params }) {
try {
const product = await fetch(`https://api.example.com/products/${params.id}`, {
next: { revalidate: 60 } // Revalidate every minute
}).then(res => {
if (!res.ok) throw new Error('Product not found');
return res.json();
});
return {
props: { product },
revalidate: 60 // ISR: 60 seconds
};
} catch (error) {
return {
notFound: true
};
}
}
On-Demand ISR with Revalidation
jsx
// pages/api/revalidate.js
export default async function handler(req, res) {
// Check for secret token
if (req.query.secret !== process.env.REVALIDATION_TOKEN) {
return res.status(401).json({ message: 'Invalid token' });
}
try {
const path = req.query.path;
// Revalidate specific path
await res.revalidate(path);
return res.json({ revalidated: true });
} catch (err) {
return res.status(500).send('Error revalidating');
}
}
// lib/revalidate.js
export async function revalidateProduct(productId) {
try {
await fetch(`/api/revalidate?secret=${process.env.REVALIDATION_TOKEN}&path=/products/${productId}`);
console.log(`Revalidated product ${productId}`);
} catch (error) {
console.error('Revalidation failed:', error);
}
}
// app/actions/product-actions.js
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
export async function updateProduct(productId, formData) {
try {
// Update product in database
await fetch(`https://api.example.com/products/${productId}`, {
method: 'PUT',
body: JSON.stringify(Object.fromEntries(formData))
});
// Revalidate multiple ways
revalidatePath('/products'); // Revalidate products list
revalidatePath(`/products/${productId}`); // Revalidate specific product page
revalidateTag('products'); // Revalidate all data with this tag
return { success: true };
} catch (error) {
return { error: error.message };
}
}
Advanced ISR Patterns
jsx
// pages/blog/[slug].js - ISR with fallback
export async function getStaticPaths() {
// Only pre-render featured posts
const featuredPosts = await fetch('https://api.example.com/posts/featured')
.then(res => res.json());
const paths = featuredPosts.map(post => ({
params: { slug: post.slug }
}));
return {
paths,
fallback: true // Show loading state for non-pre-rendered pages
};
}
export async function getStaticProps({ params }) {
try {
const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
next: { revalidate: 300 } // 5 minutes
}).then(res => {
if (!res.ok) return null;
return res.json();
});
if (!post) {
return {
notFound: true,
revalidate: 60 // Try again in 1 minute
};
}
return {
props: { post },
revalidate: 300 // 5 minutes
};
} catch (error) {
return {
notFound: true,
revalidate: 60
};
}
}
export default function BlogPost({ post }) {
const router = useRouter();
// Show loading state for fallback pages
if (router.isFallback) {
return <div>Loading...</div>;
}
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}
ISR with Time-Based and On-Demand Revalidation
jsx
// pages/news/[category].js
export async function getStaticPaths() {
const categories = await fetch('https://api.example.com/news/categories')
.then(res => res.json());
const paths = categories.map(category => ({
params: { category: category.slug }
}));
return {
paths,
fallback: 'blocking'
};
}
export async function getStaticProps({ params }) {
const news = await fetch(`https://api.example.com/news/${params.category}`, {
next: {
revalidate: 60, // Time-based: 1 minute
tags: [`news-${params.category}`] // For on-demand revalidation
}
}).then(res => res.json());
return {
props: {
news,
category: params.category,
lastUpdated: new Date().toISOString()
},
revalidate: 60 // ISR
};
}
// API route for on-demand revalidation
// pages/api/revalidate-news.js
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method not allowed' });
}
const { category, secret } = req.body;
if (secret !== process.env.REVALIDATION_SECRET) {
return res.status(401).json({ message: 'Invalid secret' });
}
try {
// Revalidate specific news category
await res.revalidate(`/news/${category}`);
// Also revalidate news homepage
await res.revalidate('/news');
return res.json({
revalidated: true,
paths: [`/news/${category}`, '/news']
});
} catch (err) {
return res.status(500).json({
message: 'Error revalidating',
error: err.message
});
}
}
Performance Optimization Summary
Choosing the Right Strategy
| Use Case | Data Fetching | Caching | Revalidation |
|---|---|---|---|
| Blog posts | SSG/ISR | revalidate: 3600 |
On-demand + time-based |
| User dashboard | SSR/Client | cache: 'no-store' |
Real-time |
| E-commerce products | ISR | revalidate: 300 |
On-demand |
| Analytics | Client | SWR polling | Real-time |
| News feed | ISR | revalidate: 60 |
Frequent time-based |
Best Practices
-
Use SSG/ISR for public, cacheable content
-
Use SSR for personalized, user-specific data
-
Use Client-side for real-time updates and user interactions
-
Implement proper cache headers and revalidation strategies
-
Use Server Actions for mutations with automatic revalidation
-
Monitor cache hit ratios and adjust revalidation times accordingly
This extensive guide covers all aspects of Next.js data fetching and caching, providing you with the tools to build highly performant applications with optimal caching strategies.
Comments (0)
No comments yet. Be the first to comment!