Next.js Data Fetching & Caching
name: data-fetching description: "Next.js data fetching strategies and caching patterns. Covers Server Component fetching, React cache(), Next.js caching layers, revalidation strategies (time-based, on-demand, tag-based), ISR, SWR and TanStack Query for client-side, parallel vs sequential fetching, and Streaming with Suspense." license: MIT metadata: author: Balazs Barta version: "0.1.0"
Overview
Effective data fetching in Next.js requires understanding multiple caching layers and when to use server-side vs client-side fetching. This skill covers:
- •Server Component data fetching patterns
- •Next.js 4-layer caching architecture
- •Revalidation strategies (time-based, on-demand, tag-based)
- •Parallel and sequential data fetching
- •Client-side fetching with SWR and TanStack Query
- •Streaming with Suspense boundaries
Quick Lookup: Next.js Documentation
Always check the Next.js llms.txt first for:
- •Data Fetching API reference
- •Caching and Revalidation docs
- •Route Handlers and Server Components
Server Component Data Fetching
Server Components allow direct database or API queries without exposing credentials:
Basic Fetch
// app/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
return posts;
}
export default async function Page() {
const posts = await getPosts();
return (
<div>
{posts.map(post => (
<article key={post.id}>{post.title}</article>
))}
</div>
);
}
Database Query
// app/page.tsx
import { db } from '@/lib/db';
async function getPosts() {
// Direct database query - credentials never exposed to client
return await db.posts.findAll();
}
export default async function Page() {
const posts = await getPosts();
return (
<div>
{posts.map(post => (
<article key={post.id}>{post.title}</article>
))}
</div>
);
}
Next.js Caching Layers
Next.js uses 4 independent caching layers:
1. Request Memoization
React automatically deduplicates fetch() calls with the same URL during a single render pass:
// app/page.tsx
async function getUser() {
const res = await fetch('https://api.example.com/user/1');
return res.json();
}
async function getUserPosts() {
// Even though fetch is called in two places, only one request is made
const user = await getUser();
return await fetch(`https://api.example.com/user/${user.id}/posts`);
}
export default async function Page() {
// These two calls both fetch the user, but result in only 1 HTTP request
const user = await getUser();
const posts = await getUserPosts();
return <div>{user.name}</div>;
}
2. Data Cache (Persistent)
The Data Cache stores responses across deployments. By default, all fetch() calls are cached:
// This response is cached indefinitely
async function getPosts() {
const res = await fetch('https://api.example.com/posts');
return res.json();
}
Opt out of Data Cache:
// Skip caching for this fetch
const res = await fetch('https://api.example.com/posts', {
cache: 'no-store' // Don't cache this request
});
3. Full Route Cache
Next.js pre-renders entire routes at build time and serves cached HTML/RSC:
// app/blog/page.tsx
// This entire page is cached at build time
export default async function Page() {
const posts = await getPosts();
return <div>{/* ... */}</div>;
}
Disable Full Route Cache:
// This page is never cached
export const dynamic = 'force-dynamic';
export default async function Page() {
const posts = await getPosts();
return <div>{/* ... */}</div>;
}
4. Router Cache (Browser)
Client-side cache of React Server Component payloads:
// Automatically cached in browser for 30 seconds
export default async function Page() {
const posts = await getPosts();
return <div>{/* ... */}</div>;
}
Disable Router Cache:
export const revalidate = 0; // Don't cache in browser
Revalidation Strategies
Time-Based Revalidation
Revalidate data after a specific time interval:
// app/blog/page.tsx
// Revalidate this page every 60 seconds
export const revalidate = 60;
async function getPosts() {
const res = await fetch('https://api.example.com/posts');
return res.json();
}
export default async function Page() {
const posts = await getPosts();
return <div>{/* ... */}</div>;
}
Also works per-fetch:
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 } // Revalidate every 60 seconds
});
return res.json();
}
On-Demand Revalidation (Path-Based)
Manually trigger revalidation for a specific path:
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const post = await db.posts.create({
title: formData.get('title') as string,
});
// Revalidate the /blog page
revalidatePath('/blog');
return post;
}
On-Demand Revalidation (Tag-Based)
Tag fetches and revalidate by tag:
// app/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] } // Tag this fetch
});
return res.json();
}
Revalidate by tag:
// app/actions.ts
'use server';
import { revalidateTag } from 'next/cache';
export async function createPost(formData: FormData) {
await db.posts.create({
title: formData.get('title') as string,
});
revalidateTag('posts'); // Revalidate all fetches tagged 'posts'
return { success: true };
}
Incremental Static Regeneration (ISR)
Generate static pages on demand and revalidate them:
// app/blog/[slug]/page.tsx
export const revalidate = 3600; // Revalidate every hour
export async function generateStaticParams() {
// Generate pages for these slugs at build time
const posts = await db.posts.findAll();
return posts.map(post => ({ slug: post.slug }));
}
export async function generateMetadata({ params }: any) {
const post = await db.posts.findBySlug(params.slug);
return { title: post.title };
}
export default async function Page({ params }: any) {
const post = await db.posts.findBySlug(params.slug);
return <article>{post.content}</article>;
}
Parallel Data Fetching
Fetch multiple resources concurrently:
// app/dashboard/page.tsx
async function getUser() {
const res = await fetch('https://api.example.com/user');
return res.json();
}
async function getPosts() {
const res = await fetch('https://api.example.com/posts');
return res.json();
}
async function getComments() {
const res = await fetch('https://api.example.com/comments');
return res.json();
}
export default async function Dashboard() {
// These run in parallel, not sequentially
const [user, posts, comments] = await Promise.all([
getUser(),
getPosts(),
getComments(),
]);
return <div>{/* ... */}</div>;
}
Sequential Data Fetching
When you need data from one request to make the next:
// app/user/[id]/page.tsx
async function getUser(userId: string) {
const res = await fetch(`https://api.example.com/user/${userId}`);
return res.json();
}
async function getUserPosts(userId: string) {
const res = await fetch(`https://api.example.com/user/${userId}/posts`);
return res.json();
}
export default async function UserPage({ params }: any) {
// First request
const user = await getUser(params.id);
// Second request depends on first
const posts = await getUserPosts(user.id);
return <div>{/* ... */}</div>;
}
Streaming with Suspense
Stream content to the client progressively while data is being fetched:
// app/blog/page.tsx
import { Suspense } from 'react';
async function PostsList() {
// This might be slow
const posts = await fetchPostsSlowly();
return (
<div>
{posts.map(post => (
<article key={post.id}>{post.title}</article>
))}
</div>
);
}
function PostsLoadingFallback() {
return <div>Loading posts...</div>;
}
export default function Page() {
return (
<div>
<h1>Blog</h1>
<Suspense fallback={<PostsLoadingFallback />}>
<PostsList />
</Suspense>
</div>
);
}
Nested Suspense boundaries:
// app/dashboard/page.tsx
import { Suspense } from 'react';
function UserSkeleton() {
return <div className="skeleton">Loading user...</div>;
}
function PostsSkeleton() {
return <div className="skeleton">Loading posts...</div>;
}
async function UserCard() {
const user = await getUser();
return <div>{user.name}</div>;
}
async function PostsList() {
const posts = await getPosts();
return <div>{posts.length} posts</div>;
}
export default function Dashboard() {
return (
<div>
<Suspense fallback={<UserSkeleton />}>
<UserCard />
</Suspense>
<Suspense fallback={<PostsSkeleton />}>
<PostsList />
</Suspense>
</div>
);
}
Client-Side Data Fetching
Use client-side fetching for:
- •Real-time data that updates frequently
- •User-specific data
- •Data based on user interactions
'use client';
import { useEffect, useState } from 'react';
export default function Posts() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/posts')
.then(res => res.json())
.then(data => {
setPosts(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{posts.map(post => (
<article key={post.id}>{post.title}</article>
))}
</div>
);
}
SWR (stale-while-revalidate)
Lightweight client-side fetching library with automatic revalidation:
'use client';
import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then(res => res.json());
export default function Posts() {
const { data: posts, error, isLoading } = useSWR('/api/posts', fetcher);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{posts.map(post => (
<article key={post.id}>{post.title}</article>
))}
</div>
);
}
Key features:
- •Automatic revalidation (by default every 2 seconds while focused)
- •Request deduplication
- •Pagination support
- •Optimistic mutations
TanStack Query (React Query)
Powerful client-side data fetching and caching library:
'use client';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function usePosts() {
return useQuery({
queryKey: ['posts'],
queryFn: async () => {
const res = await fetch('/api/posts');
return res.json();
},
staleTime: 60000, // 1 minute
});
}
export default function Posts() {
const { data: posts, isLoading, error } = usePosts();
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{posts.map(post => (
<article key={post.id}>{post.title}</article>
))}
</div>
);
}
With mutations:
'use client';
import { useMutation, useQueryClient } from '@tanstack/react-query';
export default function CreatePostForm() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: async (newPost) => {
const res = await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify(newPost),
});
return res.json();
},
onSuccess: () => {
// Revalidate posts query
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
return (
<form onSubmit={(e) => {
e.preventDefault();
mutation.mutate({ title: 'New Post' });
}}>
<button disabled={mutation.isPending}>
{mutation.isPending ? 'Creating...' : 'Create Post'}
</button>
</form>
);
}
SWR vs TanStack Query
| Feature | SWR | TanStack Query |
|---|---|---|
| Bundle Size | ~13kb | ~32kb |
| Complexity | Simpler | More powerful |
| Mutations | Basic | Advanced with invalidation |
| Caching | Simpler | Granular control |
| Pagination | Built-in helpers | Manual setup |
| Best For | Simple apps | Complex enterprise apps |
Common Data Fetching Mistakes
Waterfall Requests (Slow)
// ❌ BAD: Sequential requests
export default async function Page() {
const user = await getUser(); // Wait for this
const posts = await getPosts(user.id); // Then do this
const comments = await getComments(); // Then this
return <div>{/* ... */}</div>;
}
Parallel Requests (Fast)
// ✅ GOOD: Fetch in parallel
export default async function Page() {
const [user, posts, comments] = await Promise.all([
getUser(),
getPosts(),
getComments(),
]);
return <div>{/* ... */}</div>;
}
Over-Fetching Data
// ❌ BAD: Fetch everything
const allUsers = await db.users.findAll(); // 1000s of users
// ✅ GOOD: Fetch only what you need
const users = await db.users.findAll({ limit: 10, offset: 0 });
Wrong Caching Strategy
// ❌ BAD: Caching dynamic data
export const revalidate = 3600;
async function getCurrentUser() {
return await db.users.findById(currentUserId); // Changes per request!
}
// ✅ GOOD: Don't cache user-specific data
export const dynamic = 'force-dynamic';
async function getCurrentUser() {
return await db.users.findById(currentUserId);
}
Resources
For detailed patterns and examples, see:
- •
/skills/data-fetching/references/caching-strategies.md- Deep dive into caching layers - •
/skills/data-fetching/references/client-fetching.md- Client-side fetching patterns with SWR and TanStack Query