Data fetching + caching cheatsheet.
fetch in server components
async function Page() {
const data = await fetch("https://api.example.com/users").then(r => r.json());
return <ul>{data.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}
By default Next caches successful GET responses.
fetch options
fetch(url, { cache: "force-cache" }); // memo + persisted (default in builds)
fetch(url, { cache: "no-store" }); // always fresh
fetch(url, { next: { revalidate: 60 } }); // ISR
fetch(url, { next: { tags: ["users"] } }); // tag-based
ISR per route
export const revalidate = 60;
Applied to all fetch calls in this route without explicit override.
revalidatePath / revalidateTag
"use server";
import { revalidatePath, revalidateTag } from "next/cache";
export async function createPost(formData: FormData) {
await db.post.create({ ... });
revalidatePath("/posts");
// OR
revalidateTag("users");
}
Triggers re-render of cached pages on demand.
Parallel data fetching
async function Page() {
const [user, posts] = await Promise.all([
fetchUser(),
fetchPosts(),
]);
return <View user={user} posts={posts} />;
}
Sequential awaits would block — Promise.all is much faster.
Streaming with Suspense
import { Suspense } from "react";
async function User() {
const u = await fetchUser(); // slow
return <p>{u.name}</p>;
}
async function Posts() {
const ps = await fetchPosts(); // slow
return <ul>{ps.map(...)}</ul>;
}
export default function Page() {
return (
<>
<h1>Profile</h1>
<Suspense fallback={<p>loading user…</p>}>
<User />
</Suspense>
<Suspense fallback={<p>loading posts…</p>}>
<Posts />
</Suspense>
</>
);
}
User and Posts stream in independently.
cache() — request-scoped memoization
import { cache } from "react";
export const getUser = cache(async (id: number) => {
return db.user.find(id);
});
Within one request, multiple getUser(1) calls fetch once.
unstable_cache — persisted cache
import { unstable_cache } from "next/cache";
export const getTopPosts = unstable_cache(
async () => db.post.findMany({ orderBy: { views: "desc" }, take: 10 }),
["top-posts"],
{ revalidate: 300, tags: ["posts"] },
);
Persists across requests.
No-cache patterns
import { cookies, headers } from "next/headers";
async function Page() {
cookies(); // forces dynamic rendering
return <p>...</p>;
}
Using cookies/headers opts into dynamic rendering.
Reading cookies
import { cookies } from "next/headers";
async function Page() {
const c = await cookies();
const session = c.get("session")?.value;
...
}
Setting cookies (server action / route handler)
"use server";
import { cookies } from "next/headers";
export async function login(formData: FormData) {
const c = await cookies();
c.set("session", "...", { httpOnly: true, secure: true, sameSite: "lax" });
}
Reading headers
import { headers } from "next/headers";
async function Page() {
const h = await headers();
const ua = h.get("user-agent");
}
With TanStack Query (client)
"use client";
import { useQuery } from "@tanstack/react-query";
export function Users() {
const { data } = useQuery({
queryKey: ["users"],
queryFn: () => fetch("/api/users").then(r => r.json()),
});
return <ul>{data?.map(...)}</ul>;
}
Hydrate from server:
// page.tsx (server)
const qc = new QueryClient();
await qc.prefetchQuery({ queryKey: ["users"], queryFn: fetchUsers });
return (
<HydrationBoundary state={dehydrate(qc)}>
<Users />
</HydrationBoundary>
);
Database queries directly
import { db } from "@/lib/db";
async function Page() {
const users = await db.user.findMany({ take: 10 });
return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}
No HTTP roundtrip — direct DB access from server components.
Auth-gated data
import { redirect } from "next/navigation";
import { getSession } from "@/lib/auth";
async function Page() {
const session = await getSession();
if (!session) redirect("/login");
const user = await db.user.find(session.userId);
return <Profile user={user} />;
}
Common mistakes
- Sequential awaits instead of
Promise.all. - Forgetting
revalidateafter mutations → stale data. - Using
unstable_cachewith values that change per user. force-dynamiceverywhere → loses caching benefits.fetchwithAuthorizationheader withoutcache: "no-store"— caches per-user data.
Read this next
If you want my data fetching + cache patterns, they’re at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .