
Data Fetching & Caching
š Your Next.js app is slower than it could be. This is why
It's not your database, it's not your API. It because you're not unlocking the full power of Next.js data fetching and caching patterns.
I built this cheatsheet to help you fetch and cache data in Next.js with the best practices.
ā
Fetching in server components
ā
Parallel and sequential fetching
ā
Fetching in client components
ā
Enabling Cache Components
ā
use cache
ā
cacheLife
ā
cacheTag
ā
On-demand revalidation
ā
Server actions
ā
Streaming uncached data
#nextjs #react #datafetching #caching #usecache #serveractions #webdev #frontend #coding #tips
Fetching in server components
Fetch directly in an async server component. No useEffect needed.
export default async function Posts() {
const res = await fetch("https://api.example.com/posts");
const posts = await res.json();
return <PostList posts={posts} />;
}Requests to the same URL are automatically deduplicated during a render pass.
Parallel and sequential fetching
Start requests in parallel to avoid waterfalls.
// ā
Parallel: start both, then await both
const userPromise = getUser(id);
const postsPromise = getPosts(id);
const [user, posts] = await Promise.all([userPromise, postsPromise]);// ā Sequential: posts waits for user
const user = await getUser(id);
const posts = await getPosts(id);Fetching in client components
Use React's use API to stream data from the server. Fetch in a server component and pass the promise down without awaiting it.
Server component:
import { Suspense } from "react";
export default function Page() {
const posts = getPosts(); // don't await
return (
<Suspense fallback={<div>Loading...</div>}>
<Posts posts={posts} />
</Suspense>
);
}Client component:
"use client";
import { use } from "react";
export default function Posts({ posts }) {
const allPosts = use(posts); // unwraps the promise
return <PostList posts={allPosts} />;
}For polling, mutations, and client-side cache management, use a community library like SWR or React Query.
Enabling Cache Components
Opt in via next.config.ts.
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
};
export default nextConfig;use cache
Cache the return value of an async function or component. Arguments become part of the cache key.
import { cacheLife } from "next/cache";Data-level: cache a function.
export async function getUsers() {
"use cache";
cacheLife("hours");
return db.query("SELECT * FROM users");
}UI-level: cache an entire component.
export default async function Page() {
"use cache";
cacheLife("hours");
const users = await getUsers();
return <UserList users={users} />;
}Add "use cache" at the top of a file to cache all its exports.
cacheLife
Set the cache lifetime with a profile or a custom config.
"use cache";
cacheLife("hours");| Profile | Stale | Revalidate | Expire |
|---|---|---|---|
seconds | 30s | 1s | 60s |
minutes | 5m | 1m | 1h |
hours | 5m | 1h | 1d |
days | 5m | 1d | 1w |
weeks | 5m | 1w | 30d |
max | 5m | 30d | 1y |
Fine-grained control with an object.
"use cache";
cacheLife({
stale: 3600, // fresh for 1 hour
revalidate: 7200, // revalidate after 2 hours
expire: 86400, // expire after 1 day
});cacheTag
Tag cached data so it can be invalidated on demand.
import { cacheLife, cacheTag } from "next/cache";
export async function getPosts() {
"use cache";
cacheLife("max");
cacheTag("posts");
return db.query("SELECT * FROM posts");
}Reuse the same tag across functions to invalidate them together.
On-demand revalidation
Invalidate tags from a server action or route handler.
"use server";
import { revalidateTag, updateTag, revalidatePath } from "next/cache";
export async function createPost(formData) {
const post = await db.post.create({ ... });
updateTag("posts"); // user sees their change immediately
revalidatePath("/blog"); // or invalidate by path
}| Function | Behavior | Where |
|---|---|---|
updateTag() | Expires immediately | Server actions only |
revalidateTag() | Stale-while-revalidate | Server actions, route handlers |
revalidatePath() | Invalidates a whole path | Server actions, route handlers |
revalidateTag("posts", "max"); // longest stale windowPrefer tags over paths. They are more precise and avoid over-invalidating.
Server actions
Mutate data with functions marked "use server". Call them from forms or events.
app/actions.ts
"use server";
import { updateTag } from "next/cache";
export async function createPost(formData) {
const title = formData.get("title");
await db.post.create({ data: { title } });
updateTag("posts");
}app/blog/new/page.tsx
import { createPost } from "../actions";
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" required />
<button type="submit">Create</button>
</form>
);
}Forms work without JavaScript thanks to progressive enhancement.
Streaming uncached data
Skip use cache for data that must be fresh on every request. Wrap it in Suspense instead.
import { Suspense } from "react";
async function LatestPosts() {
const res = await fetch("https://api.example.com/posts");
const posts = await res.json();
return <PostList posts={posts} />;
}
export default function Page() {
return (
<>
<h1>My Blog</h1> {/* static shell */}
<Suspense fallback={<p>Loading posts...</p>}>
<LatestPosts /> {/* streams at request time */}
</Suspense>
</>
);
}Runtime APIs like cookies(), headers(), and searchParams also stream behind Suspense.
Full-Stack AI Developer Roadmap
From HTML & CSS to working with AI models, all in one structured roadmap.