Cache Components (Next.js) — Skill
Name: cache-components
Purpose: Build correct cached/dynamic boundaries in Next.js App Router when Cache Components or PPR are in use.
Use this skill to avoid request-context leaks and to enforce proper cache invalidation.
Applies when: cacheComponents: true, Partial Prerendering (PPR), 'use cache', cacheLife, cacheTag, updateTag, revalidateTag.
Do not use when: Working in the Pages Router or when Cache Components/PPR are not enabled.
Rules
- Cached vs dynamic: Shared data should be cached; request/user-specific data must be dynamic and streamed behind
<Suspense>. - No request context inside cache: Never call
cookies(),headers(), or auth/session inside a'use cache'scope. - Cached functions must be async: Any
'use cache'function/component must beasync. - Prefer code-local caching: Favor
'use cache',cacheLife,cacheTagover route-segment config (revalidate,dynamic). - Mutations must invalidate tags: Use
updateTagfor immediate consistency orrevalidateTagfor background refresh. - PPR + generateStaticParams: Do not return empty arrays; keep request-specific logic out of the shell.
- Instant Navigation (16.3): with
partialPrefetching: true, Next.js prefetches one reusable shell per route. Every serverawaitis a Stream (<Suspense>) / Cache ('use cache') / Block (export const instant = false+ reason comment) decision; Instant Insights errors link the canonical fix atnextjs.org/docs/messages/...— apply that pattern, don't improvise. - Prefetch escalation:
<Link prefetch={true}>extends prefetching to build-time-known cached content on that link;export const prefetch = 'allow-runtime'extends it to request-time cached content and requires PR justification.
Workflow
- Classify each data dependency as shared or request-specific.
- For shared data, add
'use cache'+cacheTag(andcacheLifeif needed). - For request/user-specific data, keep it dynamic and render behind
<Suspense>. - Split cached logic from request logic if needed.
- Invalidate tags after mutations.
Checklists
Implementation checklist
- Shared data uses
'use cache' - Cached scopes have
cacheTag - No request data inside cached scopes
- Dynamic UI is isolated behind
<Suspense> - Cached functions are
async - Route stays instant (Stream/Cache), or Blocks explicitly via
export const instant = falsewith a reason - Mutations invalidate correct tags
Review checklist
- Route segment config avoided unless required
- PPR shells do not include request-specific logic
Minimal examples
Cached function
"use cache";
import { cacheLife, cacheTag } from "next/cache";
export async function getProducts(category: string) {
cacheLife("minutes");
cacheTag("products");
cacheTag(`products:${category}`);
}
Dynamic Suspense boundary
import { Suspense } from "react";
export default function Page() {
return (
<>
<MainCached />
<Suspense fallback={null}>
<UserPanel />
</Suspense>
</>
);
}
Mutation invalidation
"use server";
import { updateTag } from "next/cache";
export async function updateProduct(id: string) {
updateTag(`product:${id}`);
updateTag("products");
}
Common mistakes / pitfalls
- Reading cookies/headers/session inside
'use cache' - Missing cache tags on cached functions
- Rendering request-specific data outside
<Suspense> - Forgetting to invalidate tags after mutations
- Returning an empty array from
generateStaticParams