From laguagu-claude-code-nextjs-skills
Expert guidance for Next.js Cache Components and Partial Prerendering (PPR). Use when implementing 'use cache' directive, configuring cache lifetimes with cacheLife(), tagging cached data with cacheTag(), invalidating caches with updateTag()/revalidateTag(), optimizing static vs dynamic content boundaries, managing 'use cache: private' for compliance scenarios, pass-through/interleaving patterns, GET Route Handler caching, debugging cache issues, and reviewing Cache Component implementations.
npx claudepluginhub joshuarweaver/cascade-code-languages-misc-1 --plugin laguagu-claude-code-nextjs-skillsThis skill uses the workspace's default tool permissions.
> **Auto-activation**: Activate this skill automatically in Next.js projects that have
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
Auto-activation: Activate this skill automatically in Next.js projects that have
cacheComponents: 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/null
If 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:
Does the component fetch data or perform I/O?
Does it depend on request context (cookies(), headers(), searchParams)?
(No request context) Is the data the same across users?
'use cache' with cacheTag() and cacheLife().<Suspense> so the dynamic part streams at request time.(Has request context) Can you extract the runtime data as function arguments?
cookies()/headers() outside the cached scope, pass values
into a 'use cache' function, and wrap the dynamic caller in <Suspense>.'use cache: private'
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 form
revalidateTag('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 boundary
Solution: Wrap dynamic components in <Suspense>:
<Suspense fallback={<Skeleton />}>
<ComponentThatUsesCookies />
</Suspense>
Error: Accessing uncached data outside Suspense
Solution: 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:
async - All cached functions must be async'use cache' first - Must be first statement in function bodycacheLife() early - Should follow 'use cache' directivecookies()/headers() outside cached scope<Suspense> for non-cached async components'use cache: private' as last resort - Only when runtime data cannot be extracted as params AND compliance requires no cross-request sharingWhen 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 }