cache-components — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited cache-components (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Auto-activation: Activate this skill automatically in Next.js projects that havecacheComponents: trueinnext.config.ts/next.config.js. When detected, apply Cache Components patterns to all Server Component authoring, data fetching, and caching decisions.
When starting work in a Next.js project, check if Cache Components are enabled:
# Check next.config.ts or next.config.js for cacheComponents
grep -r "cacheComponents" next.config.* 2>/dev/nullIf cacheComponents: true is found, apply this skill's patterns proactively when:
Cache Components enable Partial Prerendering (PPR) - mixing static HTML shells with dynamic streaming content for optimal performance. Cache Components also enable state preservation during navigation with React's <Activity> component, which can keep cached component trees mounted but hidden.
Cache Components represents a shift from segment configuration to compositional code:
| Before (Deprecated) | After (Cache Components) |
|---|---|
export const revalidate = 3600 | cacheLife('hours') inside 'use cache' |
export const dynamic = 'force-static' | Use 'use cache' and Suspense boundaries |
| All-or-nothing static/dynamic | Granular: static shell + cached + dynamic |
Key Principle: Components co-locate their caching, not just their data. Next.js provides build-time feedback to guide you toward optimal patterns.
┌─────────────────────────────────────────────────────┐
│ Static Shell │
│ (Sent immediately to browser) │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Header │ │ Cached │ │ Suspense │ │
│ │ (static) │ │ Content │ │ Fallback │ │
│ └─────────────┘ └─────────────┘ └──────┬──────┘ │
│ │ │
│ ┌──────▼──────┐ │
│ │ Dynamic │ │
│ │ (streams) │ │
│ └─────────────┘ │
└─────────────────────────────────────────────────────┘When writing a React Server Component, walk through these steps in order:
cookies(), headers(), searchParams)?'use cache' with cacheTag() and cacheLife().<Suspense> so the dynamic part streams at request time.cookies()/headers() outside the cached scope, pass valuesinto a 'use cache' function, and wrap the dynamic caller in <Suspense>.
'use cache: private'(experimental — not recommended for production) as a last resort, still wrapped in <Suspense>.
Key insight: 'use cache' is for data that is the _same across users_. User-specific data stays dynamic and streams through <Suspense>. Reach for 'use cache: private' only when you cannot refactor runtime data into arguments.
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig// Cached component - output included in static shell
async function CachedPosts() {
'use cache'
const posts = await db.posts.findMany()
return <PostList posts={posts} />
}
// Page with static + cached + dynamic content
export default async function BlogPage() {
return (
<>
<Header /> {/* Static */}
<CachedPosts /> {/* Cached */}
<Suspense fallback={<Skeleton />}>
<DynamicComments /> {/* Dynamic - streams */}
</Suspense>
</>
)
}Server Actions are for MUTATIONS ONLY - never for data fetching:
| Purpose | Use | Example Functions |
|---|---|---|
| Data fetch | Server Component / 'use cache' | getProducts(), getUser(id) |
| Mutation | Server Action ('use server') | createProduct(), updateUser(), deletePost() |
"use server"
export async function getProducts() {
return await db.products.findMany() // NO! This is not a mutation
}
"use server"
export async function getTheme() {
return (await cookies()).get("theme")?.value // NO! Just reading data
}// data/products.ts - Cached data function
export async function getProducts() {
"use cache"
cacheTag("products")
cacheLife("hours")
return await db.products.findMany()
}
// page.tsx - Server Component reads data directly
import { cookies } from "next/headers"
export default async function Page() {
const products = await getProducts()
const theme = (await cookies()).get("theme")?.value ?? "light"
return <ProductList products={products} theme={theme} />
}"use server"
import { updateTag } from "next/cache"
export async function createProduct(formData: FormData) {
await db.products.create({ data: formData })
updateTag("products") // Invalidate cache after mutation
}'use cache' DirectiveMarks code as cacheable. Can be applied at three levels:
// File-level: All exports are cached
'use cache'
export async function getData() {
/* ... */
}
export async function Component() {
/* ... */
}
// Component-level
async function UserCard({ id }: { id: string }) {
'use cache'
const user = await fetchUser(id)
return <Card>{user.name}</Card>
}
// Function-level
async function fetchWithCache(url: string) {
'use cache'
return fetch(url).then((r) => r.json())
}Important: All cached functions must be async.
cacheLife() - Control Cache Durationimport { cacheLife } from 'next/cache'
async function Posts() {
'use cache'
cacheLife('hours') // Use a predefined profile
// Or custom configuration:
cacheLife({
stale: 60, // 1 min - client cache validity
revalidate: 3600, // 1 hr - start background refresh
expire: 86400, // 1 day - absolute expiration
})
return await db.posts.findMany()
}Predefined profiles: 'default', 'seconds', 'minutes', 'hours', 'days', 'weeks', 'max'
cacheTag() - Tag for Invalidationimport { cacheTag } from 'next/cache'
async function BlogPosts() {
'use cache'
cacheTag('posts')
cacheLife('days')
return await db.posts.findMany()
}
async function UserProfile({ userId }: { userId: string }) {
'use cache'
cacheTag('users', `user-${userId}`) // Multiple tags
return await db.users.findUnique({ where: { id: userId } })
}updateTag() - Immediate InvalidationFor read-your-own-writes semantics:
'use server'
import { updateTag } from 'next/cache'
export async function createPost(formData: FormData) {
await db.posts.create({ data: formData })
updateTag('posts') // Client immediately sees fresh data
}revalidateTag() - Background RevalidationFor stale-while-revalidate pattern:
'use server'
import { revalidateTag } from 'next/cache'
export async function updatePost(id: string, data: FormData) {
await db.posts.update({ where: { id }, data })
revalidateTag('posts', 'max') // Serve stale, refresh in background
}⚠️ Deprecated: The single-argument formrevalidateTag('posts')is deprecated. Always pass a profile ('max'is recommended for stale-while-revalidate) or{ expire: <seconds> }as the second argument. For webhooks that require immediate expiration, userevalidateTag(tag, { expire: 0 }). For immediate read-your-own-writes in Server Actions, preferupdateTag()instead.
| Content Type | API | Behavior |
|---|---|---|
| Static | No directive | Rendered at build time |
| Cached | 'use cache' | Included in static shell, revalidates |
| Dynamic | Inside <Suspense> | Streams at request time |
Critical Concept: With Cache Components, Next.js renders ALL permutations of provided parameters to create reusable subshells.
// app/products/[category]/[slug]/page.tsx
export async function generateStaticParams() {
return [
{ category: 'jackets', slug: 'classic-bomber' },
{ category: 'jackets', slug: 'essential-windbreaker' },
{ category: 'accessories', slug: 'thermal-fleece-gloves' },
]
}Next.js renders these routes:
/products/jackets/classic-bomber ← Full params (complete page)
/products/jackets/essential-windbreaker ← Full params (complete page)
/products/accessories/thermal-fleece-gloves ← Full params (complete page)
/products/jackets/[slug] ← Partial params (category subshell)
/products/accessories/[slug] ← Partial params (category subshell)
/products/[category]/[slug] ← No params (fallback shell)Why this matters: The category subshell (/products/jackets/[slug]) can be reused for ANY jacket product, even ones not in generateStaticParams. Users navigating to an unlisted jacket get the cached category shell immediately, with product details streaming in.
generateStaticParams RequirementsWith Cache Components enabled:
// ❌ ERROR with Cache Components
export function generateStaticParams() {
return [] // Build error: must provide at least one param
}
// ✅ CORRECT: Provide real params
export async function generateStaticParams() {
const products = await getPopularProducts()
return products.map(({ category, slug }) => ({ category, slug }))
}Arguments become part of the cache key:
// Different userId = different cache entry
async function UserData({ userId }: { userId: string }) {
'use cache'
cacheTag(`user-${userId}`)
return await fetchUser(userId)
}Cache Components provides early feedback during development. These build errors guide you toward optimal patterns:
Error: Accessing cookies/headers/searchParams outside a Suspense boundarySolution: Wrap dynamic components in <Suspense>:
<Suspense fallback={<Skeleton />}>
<ComponentThatUsesCookies />
</Suspense>Error: Accessing uncached data outside SuspenseSolution: Either cache the data or wrap in Suspense:
// Option 1: Cache it
async function ProductData({ id }: { id: string }) {
'use cache'
return await db.products.findUnique({ where: { id } })
}
// Option 2: Make it dynamic with Suspense
;<Suspense fallback={<Loading />}>
<DynamicProductData id={id} />
</Suspense>Error: Cannot access cookies/headers inside 'use cache'Solution: Extract runtime data outside cache boundary (see "Handling Runtime Data" above).
When generating Cache Component code:
'use cache' directivecookies()/headers() outside cached scope<Suspense> for non-cached async componentsWhen reviewing code in Cache Components projects, flag these issues:
'use cache' where caching would benefitcacheTag() calls (makes invalidation impossible)cacheLife() (relies on defaults which may not be appropriate)updateTag()/revalidateTag() after mutationscookies()/headers() called inside 'use cache' scope<Suspense> boundariesexport const revalidate - replace with cacheLife() in 'use cache'export const dynamic - replace with Suspense + cache boundariesgenerateStaticParams() return - must provide at least one paramrevalidateTag('tag') - use two-argument form with profile or { expire }~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.