Skip to main content

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.all for parallel fetches
  • Suspense enables streaming partial content
  • Use SWR/React Query for client-side fetching
  • Fetch requests are automatically deduped