Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save adityacodepublic/ce312b17d1baf5d765100248b6eb3912 to your computer and use it in GitHub Desktop.

Select an option

Save adityacodepublic/ce312b17d1baf5d765100248b6eb3912 to your computer and use it in GitHub Desktop.

Caching diagram Next js


React cache and Next js cache

  • React.cache memoizes 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/cache goes 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 fetch API.
    • React doesn't modify globalThis.fetch, so it won't automatically deduplicate your fetch requests.

Important points:

  • react cache is used for client-side caching.
    revalidateTag is for server-side cache revalidation.

  • You should wrap your unstable_cache function in react cache.

    This way, unstable_cache will handle server-side caching, and react cache will memoize data on the client side (per request).

  • revalidating the unstable cache also revalidates the react one because RSC is refetched if there's dynamic data and the cached functions rerun beacuse it's no longer the same request


Guide to Using unstable_cache in Next.js


Key Concepts of unstable_cache

  • 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() or revalidatePath().

Example 1:

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:

  1. allow you invalidate a group of tags (ie. all products in this case). This is not possible with just the key.
  2. allow you to invalidate a specific group (ie. all the products, but not all the wordpress posts).
  3. still allow you to invalidate a specific product/blog (ie. revalidateTag("product:42"))

Example 2: Caching User Details by ID

// 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>
  );
}

Explanation:

  • 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).

Example 3: Caching Posts by Category

// 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>
  );
}

Explanation:

  • Cache Key: posts-category-${categoryId} caches each category’s posts separately.
  • Tags: Tags like category-${categoryId} and posts allow for both category-specific and global cache invalidation.
  • Revalidation: Cache revalidates every 3600 seconds (1 hour).

Example 4: Caching Products with Pagination

// 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>
  );
}

Explanation:

  • Cache Key: products-page-${page} ensures each page of products is cached separately.
  • Tags: Tags like products-page-${page} and products allow for page-specific or global product cache invalidation.
  • Revalidation: Cache revalidates every 1200 seconds (20 minutes).

Example 5: Caching with Multiple Tags

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,
        },
    )();
});

Explanation:

  • 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.

More Examples

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,
        },
    )();
});

Best Practices for Dynamic Tags

  • Tagging Dynamic Entities: Use dynamic tags like user-${id} or category-${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.

When to Use unstable_cache

  • Expensive, Dynamic Data: Use unstable_cache when fetching expensive or frequently changing data like user profiles, paginated lists, or category-based queries.
  • Custom Cache Invalidation: unstable_cache is useful when you need custom invalidation strategies through tags (e.g., revalidateTag()).

General Takeaways

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_cache does 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.

Caching Image, Notion Blog,

Examples and explanations

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment