-
React.cachememoizes functions (per server request) and stores the cache in the client router cache.React will invalidate the cache for all memoized functions on each new server request.
-
next/cachegoes a step further by persisting responses to disk, reducing the number of requests to the backend data source for all visitors. For instance, 100 page visits would trigger just 1 query.
In general, you should use React.cache in the following scenarios:
- When you're fetching the same data in multiple palces on the client or server side and not using the
fetchAPI.- React doesn't modify
globalThis.fetch, so it won't automatically deduplicate your fetch requests.
- React doesn't modify
-
react cacheis used for client-side caching.
revalidateTagis for server-side cache revalidation. -
You should wrap your
unstable_cachefunction inreact cache.This way,
unstable_cachewill handle server-side caching, andreact cachewill memoize data on the client side (per request). -
revalidating the unstable cache also revalidates the react one because
RSCisrefetchedif there's dynamic data and the cached functions rerun beacuse it's no longer the same request
- Cache Key: A unique identifier for a cache entry. It maps one-to-one to a cache item and can be dynamic (e.g.,
user-${id}). Learn more - Tags: Tags allow you to group multiple cache entries together for easier invalidation (e.g., invalidating all products or posts at once).
- Revalidation: This specifies the number of seconds after which the cache should be revalidated, or manually with functions like
revalidateTag()orrevalidatePath().
say you run a website that has a shop and a blog. There are two api's you talk to: shopify and wordpress.
- Calls to the shop could look like:
function getProductById(id) {
return unstable_cache(() => shopify.getProduct(id), ["shopify", id], {
tags: ["shopify", `shopify:${id}`],
})
}- whereas the calls to wordpress could look like:
function getBlogPostById(id) {
return unstable_cache(() => wordpress.getPost(id), ["wordpress", id], {
tags: ["wordpress" , "wordpress:${id}"],
})
}Now you add a global sale to your shop. You need to revalidate all products so the price can be updated. You can call revalidateTag("shopify").
This will:
- allow you invalidate a group of tags (ie. all products in this case). This is not possible with just the key.
- allow you to invalidate a specific group (ie. all the products, but not all the wordpress posts).
- still allow you to invalidate a specific product/blog (ie.
revalidateTag("product:42"))
// app/users/[id]/page.tsx
import { prisma } from '@/lib/prisma';
import { unstable_cache } from 'next/cache';
const fetchUserById = unstable_cache(
async (id: string) => await prisma.user.findUnique({ where: { id } }),
(id) => [`user-${id}`], // Dynamic cache key
{ tags: [`user-${id}`], revalidate: 1800 } // Dynamic tag and revalidation
);
export default async function UserPage({ params }: { params: { id: string } }) {
const user = await fetchUserById(params.id);
if (!user) return <div>User not found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}- Cache Key:
user-${id}ensures each user has its own cache entry. - Tags:
user-${id}allows you to invalidate specific users (e.g.,revalidateTag('user-123')). - Revalidation: Cache expires after 1800 seconds (30 minutes).
// app/categories/[categoryId]/page.tsx
import { prisma } from '@/lib/prisma';
import { unstable_cache } from 'next/cache';
const fetchPostsByCategory = unstable_cache(
async (categoryId: string) => {
return await prisma.post.findMany({ where: { categoryId } });
},
(categoryId) => [`posts-category-${categoryId}`], // Dynamic cache key
{ tags: [`category-${categoryId}`, 'posts'], revalidate: 3600 } // Dynamic tags
);
export default async function CategoryPostsPage({ params }: { params: { categoryId: string } }) {
const posts = await fetchPostsByCategory(params.categoryId);
return (
<div>
<h1>Posts in Category {params.categoryId}</h1>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}- Cache Key:
posts-category-${categoryId}caches each category’s posts separately. - Tags: Tags like
category-${categoryId}andpostsallow for both category-specific and global cache invalidation. - Revalidation: Cache revalidates every 3600 seconds (1 hour).
// app/products/page.tsx
import { prisma } from '@/lib/prisma';
import { unstable_cache } from 'next/cache';
const fetchPaginatedProducts = unstable_cache(
async (page: number) => {
return await prisma.product.findMany({
skip: (page - 1) * 10,
take: 10,
});
},
(page) => [`products-page-${page}`], // Paginated cache key
{ tags: [`products-page-${page}`, 'products'], revalidate: 1200 } // Dynamic tags
);
export default async function ProductsPage({ searchParams }: { searchParams: { page: string } }) {
const page = parseInt(searchParams.page || '1', 10);
const products = await fetchPaginatedProducts(page);
return (
<div>
<h1>Products - Page {page}</h1>
<ul>
{products.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</div>
);
}- Cache Key:
products-page-${page}ensures each page of products is cached separately. - Tags: Tags like
products-page-${page}andproductsallow for page-specific or global product cache invalidation. - Revalidation: Cache revalidates every 1200 seconds (20 minutes).
export const userMailboxAccess = cache((mailboxId: string, userId: string | null) => {
if (!userId) return false;
return unstable_cache(
async () => {
const mailbox = await db.query.MailboxForUser.findFirst({
where: and(eq(MailboxForUser.mailboxId, mailboxId), eq(MailboxForUser.userId, userId)),
});
return !!mailbox;
},
[mailboxId, userId],
{
tags: [
`mailbox-${mailboxId}`,
`user-${userId}`,
"user-mailbox-access",
`user-mailbox-access-${mailboxId}-${userId}`,
],
// revalidate after 7 days
revalidate: 60 * 60 * 24 * 7,
},
)();
});- Tags: Combines multiple tags (
mailbox-${mailboxId},user-${userId}) to allow for granular invalidation (e.g., invalidating specific mailboxes or users). - Revalidation: Cache is set to revalidate every 7 days.
cache wrapped around unstable_cache.
export const mailboxCategories = cache((mailboxId: string) => {
return unstable_cache(
async () => {
const categories = await db.query.MailboxCategory.findMany({
where: eq(MailboxCategory.mailboxId, mailboxId),
columns: {
id: true,
name: true,
color: true,
},
orderBy: asc(MailboxCategory.createdAt),
});
return categories;
},
[mailboxId],
{
tags: [`mailbox-${mailboxId}`, `mailbox-categories-${mailboxId}`],
// revalidate after 7 days
revalidate: 60 * 60 * 24 * 7,
},
)();
});export const mailboxAliases = cache((mailboxId: string) => {
return unstable_cache(
async () => {
const aliases = await db.query.MailboxAlias.findMany({
where: eq(MailboxAlias.mailboxId, mailboxId),
columns: {
id: true,
name: true,
default: true,
alias: true,
},
});
return {
aliases,
default: aliases.find((a) => a.default),
};
},
[mailboxId],
{
tags: [`mailbox-${mailboxId}`, `mailbox-aliases-${mailboxId}`],
// revalidate after 7 days
revalidate: 60 * 60 * 24 * 7,
},
)();
});export const userMailboxes = cache((userId: string) => {
return unstable_cache(
async () => {
const mailboxes = await db.query.MailboxForUser.findMany({
where: eq(MailboxForUser.userId, userId),
columns: {
mailboxId: true,
role: true,
},
orderBy: asc(MailboxForUser.mailboxId),
});
const mailboxesAliases = await db.query.MailboxAlias.findMany({
where: and(
inArray(
MailboxAlias.mailboxId,
mailboxes.map((m) => m.mailboxId),
),
eq(MailboxAlias.default, true),
),
columns: {
mailboxId: true,
alias: true,
},
});
return mailboxes.map((m) => ({
id: m.mailboxId,
role: m.role,
name: mailboxesAliases.find((a) => a.mailboxId === m.mailboxId)?.alias || null,
}));
},
[userId],
{
tags: [`user-${userId}`, `user-mailboxes-${userId}`],
// revalidate after 7 days
revalidate: 60 * 60 * 24 * 7,
},
)();
});- Tagging Dynamic Entities: Use dynamic tags like
user-${id}orcategory-${categoryId}to cache data with unique identifiers. This allows for granular cache invalidation. - Group Invalidation: Tags allow you to invalidate groups of cache entries with one call, such as
revalidateTag('shopify')to update all Shopify product caches. - General and Specific Tags: Combining dynamic (
user-${id}) and general tags (users) lets you invalidate either specific entries or a group of entries.
- Expensive, Dynamic Data: Use
unstable_cachewhen fetching expensive or frequently changing data like user profiles, paginated lists, or category-based queries. - Custom Cache Invalidation:
unstable_cacheis useful when you need custom invalidation strategies through tags (e.g.,revalidateTag()).
unstable_cache is unstable as in api can change (ie. in a non semver way in patch version) not unstable as in it will break :)
- Key Parts: Often unnecessary if your function signature already includes dynamic variables like
id. However, they can help Next.js detect when the function is changing. more 1, more 2 - Avoid for Headers/Cookies:
unstable_cachedoes not support dynamic headers or cookies inside cached functions, so pass any required dynamic data as arguments.
Above is the data from various sources on the web which I found useful while understanding
unstable_cache. I've tried to mention all the sources; if I missed any, please let me know.