React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

cachingdata-cacherequest-memoizationfull-route-cachereact-cache

Caching

Understand the four caching layers in Next.js -- request memoization, data cache, full route cache, and router cache.

Recipe

Quick-reference recipe card -- copy-paste ready.

// Layer 1: Request Memoization (automatic for fetch, manual for others)
// Same fetch URL is deduplicated within a single render pass
const data = await fetch("https://api.example.com/user/1"); // called in 3 components, only 1 request
 
// Manual memoization for non-fetch calls
import { cache } from "react";
export const getUser = cache(async (id: string) => {
  return db.user.findUnique({ where: { id } });
});
 
// Layer 2: Data Cache (persists across requests)
await fetch(url, { next: { revalidate: 3600 } }); // cached 1 hour
await fetch(url, { cache: "no-store" });           // skip data cache
await fetch(url, { next: { tags: ["users"] } });   // tag for invalidation
 
// Layer 3: Full Route Cache (static pages at build time)
// Automatic for routes with no dynamic data
 
// Layer 4: Router Cache (client-side, in-memory)
// Automatic for <Link> prefetching and back/forward navigation

When to reach for this: You need to understand why your data is stale, why a page is not updating, or how to optimize performance by leveraging the right cache layer.

Working Example

// lib/data.ts -- demonstrating all caching layers
import { cache } from "react";
import { db } from "@/lib/db";
 
// ---- Layer 1: Request Memoization ----
// This function can be called from multiple Server Components
// in the same render pass -- only one DB query runs
export const getCurrentUser = cache(async () => {
  const session = await getSession();
  if (!session) return null;
  return db.user.findUnique({ where: { id: session.userId } });
});
 
// ---- Layer 2: Data Cache ----
export async function getProducts() {
  const res = await fetch("https://api.example.com/products", {
    next: { revalidate: 600, tags: ["products"] },
  });
  return res.json() as Promise<Product[]>;
}
 
// No caching -- always fresh
export async function getCartItems(userId: string) {
  const res = await fetch(`https://api.example.com/cart/${userId}`, {
    cache: "no-store",
  });
  return res.json() as Promise<CartItem[]>;
}
// app/layout.tsx -- getCurrentUser called here...
import { getCurrentUser } from "@/lib/data";
 
export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const user = await getCurrentUser(); // Request 1 (actual DB call)
 
  return (
    <html lang="en">
      <body>
        <nav>
          <span>{user?.name ?? "Guest"}</span>
        </nav>
        {children}
      </body>
    </html>
  );
}
// app/dashboard/page.tsx -- ...and also called here (deduplicated)
import { getCurrentUser } from "@/lib/data";
 
export default async function DashboardPage() {
  const user = await getCurrentUser(); // Request 2 (memoized -- no DB call)
 
  return <h1>Welcome back, {user?.name}</h1>;
}

What this demonstrates:

  • React.cache deduplicating a database call across two Server Components in the same render
  • next.revalidate placing fetch results into the Data Cache
  • cache: "no-store" bypassing the Data Cache for user-specific data
  • The layout and page both calling getCurrentUser() without triggering two queries

Deep Dive

How It Works

  • Request Memoization runs during a single server render. When fetch is called with the same URL and options, React returns the memoized result. For non-fetch calls (database, ORM), wrap the function with React.cache(). Memoization is cleared after the render completes.
  • Data Cache persists fetched data across requests and deployments on the server. It is keyed by the fetch URL and options. It respects next.revalidate for time-based expiry and next.tags for on-demand invalidation via revalidateTag().
  • Full Route Cache stores the complete HTML and RSC payload for statically rendered routes at build time. When a user requests a static page, the cached version is served instantly. Dynamic routes (those using cookies(), headers(), searchParams, or cache: "no-store") are excluded.
  • Router Cache is a client-side, in-memory cache that stores prefetched route segments. When the user navigates with <Link>, the next page may already be cached. The router cache has different lifetimes: 30 seconds for dynamic pages, 5 minutes for static pages (these are defaults and configurable in Next.js 15+).

Variations

Opting out of all caching for a route:

// app/real-time/page.tsx
export const dynamic = "force-dynamic"; // skips Full Route Cache
export const fetchCache = "force-no-store"; // skips Data Cache for all fetches

Using unstable_cache for non-fetch data:

import { unstable_cache } from "next/cache";
 
const getCachedPosts = unstable_cache(
  async () => db.post.findMany(),
  ["all-posts"],        // cache key parts
  {
    tags: ["posts"],
    revalidate: 3600,
  }
);

Controlling Router Cache behavior:

// next.config.ts
import type { NextConfig } from "next";
 
const config: NextConfig = {
  experimental: {
    staleTimes: {
      dynamic: 0,   // don't cache dynamic pages on the client
      static: 300,  // cache static pages for 5 minutes
    },
  },
};
 
export default config;

TypeScript Notes

// React.cache preserves the function signature
import { cache } from "react";
 
const getUser = cache(
  async (id: string): Promise<User | null> => {
    return db.user.findUnique({ where: { id } });
  }
);
// getUser: (id: string) => Promise<User | null>
 
// Segment config types
export const dynamic: "auto" | "force-dynamic" | "force-static" | "error" = "auto";
export const revalidate: number | false = 60;
export const fetchCache: "auto" | "default-cache" | "only-cache" |
  "force-cache" | "force-no-store" | "default-no-store" | "only-no-store" = "auto";

Gotchas

  • Confusing request memoization with the Data Cache -- Request memoization is per-render and ephemeral; the Data Cache persists across requests. Fix: Use React.cache() for per-request deduplication and next.revalidate / next.tags for persistent caching.

  • Router Cache serving stale pages -- After a mutation, the client-side Router Cache may still serve the old page during back/forward navigation. Fix: Call router.refresh() in the Client Component after a Server Action, or configure staleTimes to reduce client-side cache duration.

  • cache: "force-cache" is not always the default -- In Next.js 15+, the default is cache: "auto", which may vary depending on context (dynamic functions, route config). Fix: Be explicit about caching behavior by setting cache or next.revalidate on every fetch.

  • unstable_cache key collisions -- If two unstable_cache calls share the same key parts but return different data shapes, you get corrupted cache. Fix: Use unique, descriptive key arrays.

  • Middleware and Edge Runtime do not share caches -- The Data Cache is per-deployment-region. Middleware running at the edge may not see the same cache as a Node.js server function. Fix: Be aware of cache boundaries in multi-region deployments.

Alternatives

AlternativeUse WhenDon't Use When
React.cache()You need per-request deduplication for non-fetch callsYou need data to persist across requests
unstable_cacheYou need Data Cache semantics for database or ORM callsPlain fetch with next.revalidate works
Redis or MemcachedYou need a shared cache across multiple server instancesThe built-in Next.js cache is sufficient
SWR stale-while-revalidateYou want client-side caching with automatic revalidationServer-side caching covers your needs
cache: "no-store" everywhereData must always be fresh (real-time dashboards)You can tolerate stale data for better performance

FAQs

What are the four caching layers in Next.js and how do they differ?
  • Request Memoization: per-render, deduplicates identical fetch calls within a single server render pass
  • Data Cache: persists across requests and deployments, keyed by fetch URL and options
  • Full Route Cache: stores prerendered HTML and RSC payload for static routes at build time
  • Router Cache: client-side in-memory cache for prefetched route segments during navigation
How do you deduplicate database queries that bypass automatic fetch memoization?
import { cache } from "react";
import { db } from "@/lib/db";
 
export const getUser = cache(async (id: string) => {
  return db.user.findUnique({ where: { id } });
});
  • Wrap the function with React.cache() for per-request deduplication
What is the default cache behavior for fetch in Next.js 15+?
  • The default is cache: "auto", not force-cache as in Next.js 14
  • The framework decides caching behavior based on context (dynamic functions, route config)
  • Be explicit by setting cache or next.revalidate on every fetch
How long does the Router Cache keep pages on the client side?
  • 30 seconds for dynamic pages
  • 5 minutes for static pages
  • These are defaults in Next.js 15+ and can be configured via staleTimes in next.config.ts
How do you opt a route out of all caching layers?
export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
  • force-dynamic skips the Full Route Cache
  • force-no-store skips the Data Cache for all fetches in the route
What causes the Router Cache to serve stale data after a mutation?
  • The client-side Router Cache does not automatically invalidate after Server Actions
  • Back/forward navigation may show the old page
  • Call router.refresh() in the Client Component or reduce staleTimes in config
What happens if two unstable_cache calls share the same key parts but return different data?
  • You get corrupted cache -- the wrong data shape is returned
  • Always use unique, descriptive key arrays for each unstable_cache call
  • This is a common source of hard-to-debug production bugs
How does React.cache() preserve TypeScript types?
import { cache } from "react";
 
const getUser = cache(
  async (id: string): Promise<User | null> => {
    return db.user.findUnique({ where: { id } });
  }
);
// getUser: (id: string) => Promise<User | null>
  • The wrapped function retains the exact same signature and return type
Does Middleware share the same Data Cache as Node.js server functions?
  • No. The Data Cache is per-deployment-region
  • Middleware running at the edge may not see the same cache as a Node.js server function
  • Be aware of cache boundaries in multi-region deployments
When should you use unstable_cache versus React.cache()?
  • React.cache() provides per-request deduplication only; data does not persist across requests
  • unstable_cache provides Data Cache semantics (persists across requests, supports tags and revalidation)
  • Use unstable_cache for database or ORM calls that need cross-request caching
How do you configure the Router Cache lifetime for dynamic pages?
// next.config.ts
const config: NextConfig = {
  experimental: {
    staleTimes: {
      dynamic: 0,   // don't cache dynamic pages on the client
      static: 300,  // cache static pages for 5 minutes
    },
  },
};
What is the type for the fetchCache segment config export?
export const fetchCache:
  | "auto"
  | "default-cache"
  | "only-cache"
  | "force-cache"
  | "force-no-store"
  | "default-no-store"
  | "only-no-store" = "auto";