Next.js Data Fetching Patterns
Answer
Next.js App Router provides powerful data fetching capabilities with automatic request deduplication, caching, and revalidation strategies.
Fetching in Server Components
// Server Component (default)
async function Page() {
// This runs on the server
const data = await fetch("https://api.example.com/data");
const json = await data.json();
return <div>{json.title}</div>;
}
Caching Strategies
// 1. Force Cache (default) - SSG behavior
const data = await fetch("https://api.example.com/data", {
cache: "force-cache",
});
// 2. No Store - SSR behavior (always fresh)
const data = await fetch("https://api.example.com/data", {
cache: "no-store",
});
// 3. Time-based Revalidation (ISR)
const data = await fetch("https://api.example.com/data", {
next: { revalidate: 60 }, // Revalidate every 60 seconds
});
// 4. Tag-based Revalidation
const data = await fetch("https://api.example.com/data", {
next: { tags: ["posts"] },
});
Request Deduplication
// Multiple components can fetch same data
// Next.js automatically dedupes during same render
// Component A
async function Sidebar() {
const user = await getUser(); // Request 1
return <div>{user.name}</div>;
}
// Component B
async function Header() {
const user = await getUser(); // Deduped, uses cached result
return <div>Welcome, {user.name}</div>;
}
async function getUser() {
const res = await fetch("https://api.example.com/user");
return res.json();
}
Parallel Data Fetching
// ✅ Parallel - faster
async function Page() {
const [posts, users] = await Promise.all([
fetch("/api/posts").then((r) => r.json()),
fetch("/api/users").then((r) => r.json()),
]);
return (
<>
<PostList posts={posts} />
<UserList users={users} />
</>
);
}
// ❌ Sequential - slower
async function Page() {
const posts = await fetch("/api/posts").then((r) => r.json());
const users = await fetch("/api/users").then((r) => r.json());
}
Streaming with Suspense
import { Suspense } from "react";
async function Page() {
return (
<>
<Header /> {/* Immediate */}
<Suspense fallback={<PostsSkeleton />}>
<Posts /> {/* Streams when ready */}
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<Comments /> {/* Streams independently */}
</Suspense>
</>
);
}
async function Posts() {
const posts = await fetchPosts(); // Suspends until ready
return <PostList posts={posts} />;
}
Preloading Data
import { unstable_noStore as noStore } from "next/cache";
// Preload function - start fetching early
export const preload = (id) => {
void getItem(id);
};
export async function getItem(id) {
noStore();
const data = await fetch(`/api/items/${id}`);
return data.json();
}
// Usage in layout
import { preload, getItem } from "./data";
export default async function Layout({ params, children }) {
preload(params.id); // Start fetch early
return children;
}
Client-Side Fetching
"use client";
import useSWR from "swr";
const fetcher = (url) => fetch(url).then((r) => r.json());
function Dashboard() {
const { data, error, isLoading } = useSWR("/api/stats", fetcher, {
refreshInterval: 5000, // Poll every 5s
});
if (error) return <div>Error</div>;
if (isLoading) return <Skeleton />;
return <Stats data={data} />;
}
Combining Server and Client
// Server: Initial data
async function Page() {
const initialPosts = await fetchPosts();
return <PostList initialData={initialPosts} />;
}
// Client: Real-time updates
("use client");
function PostList({ initialData }) {
const { data: posts } = useSWR("/api/posts", fetcher, {
fallbackData: initialData,
refreshInterval: 10000,
});
return posts.map((post) => <Post key={post.id} {...post} />);
}
Error Handling
// error.js - Error boundary per route
"use client";
export default function Error({ error, reset }) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
// Or handle in component
async function Page() {
try {
const data = await fetch("/api/data");
if (!data.ok) throw new Error("Failed to fetch");
return <Content data={await data.json()} />;
} catch (error) {
return <ErrorMessage error={error} />;
}
}
Key Points
- Server Components fetch on server by default
cache: 'force-cache'(SSG),'no-store'(SSR)next: { revalidate: 60 }for ISR- Use
Promise.allfor parallel fetches - Suspense enables streaming partial content
- Use SWR/React Query for client-side fetching
- Fetch requests are automatically deduped