mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
Remove embedded Claude Code plugin marketplace (#94877)
This commit is contained in:
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"name": "nextjs",
|
||||
"owner": {
|
||||
"name": "Vercel",
|
||||
"url": "https://vercel.com"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "cache-components",
|
||||
"source": "./plugins/cache-components",
|
||||
"description": "Expert guidance for Next.js Cache Components and Partial Prerendering (PPR). Proactively activates in projects with cacheComponents enabled.",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Next.js Team"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
# Next.js Claude Code Plugins
|
||||
|
||||
This directory contains Claude Code plugins for Next.js development.
|
||||
|
||||
## Using the Next.js Plugin Marketplace
|
||||
|
||||
The Next.js repository serves as a Claude Code plugin marketplace. Team members and contributors can install plugins directly from this repo.
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
# Add the Next.js marketplace
|
||||
/plugin marketplace add vercel/next.js
|
||||
|
||||
# List available plugins
|
||||
/plugin list
|
||||
|
||||
# Install a plugin
|
||||
/plugin install cache-components@nextjs
|
||||
```
|
||||
|
||||
### Available Plugins
|
||||
|
||||
| Plugin | Description |
|
||||
|--------|-------------|
|
||||
| `cache-components` | Expert guidance for Cache Components and PPR |
|
||||
|
||||
## For Team Members
|
||||
|
||||
To auto-enable plugins for everyone working in a Next.js project, add to `.claude/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"extraKnownMarketplaces": {
|
||||
"nextjs": {
|
||||
"source": {
|
||||
"source": "github",
|
||||
"repo": "vercel/next.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"enabledPlugins": {
|
||||
"cache-components@nextjs": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Creating New Plugins
|
||||
|
||||
To add a new plugin to the marketplace:
|
||||
|
||||
### 1. Create Plugin Directory
|
||||
|
||||
```bash
|
||||
mkdir -p .claude-plugin/plugins/my-plugin/.claude-plugin
|
||||
mkdir -p .claude-plugin/plugins/my-plugin/skills/my-skill
|
||||
```
|
||||
|
||||
### 2. Create Plugin Manifest
|
||||
|
||||
**File**: `.claude-plugin/plugins/my-plugin/.claude-plugin/plugin.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-plugin",
|
||||
"version": "1.0.0",
|
||||
"description": "What the plugin does",
|
||||
"author": {
|
||||
"name": "Next.js Team"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Create Skill
|
||||
|
||||
**File**: `.claude-plugin/plugins/my-plugin/skills/my-skill/SKILL.md`
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: my-skill
|
||||
description: When to use this skill
|
||||
---
|
||||
|
||||
# My Skill
|
||||
|
||||
Instructions for Claude...
|
||||
```
|
||||
|
||||
### 4. Register in Marketplace
|
||||
|
||||
Add to `.claude-plugin/marketplace.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": [
|
||||
{
|
||||
"name": "my-plugin",
|
||||
"source": "./plugins/my-plugin",
|
||||
"description": "What it does"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Test Locally
|
||||
|
||||
```bash
|
||||
claude --plugin-dir .claude-plugin/plugins/my-plugin
|
||||
```
|
||||
|
||||
## Plugin Structure
|
||||
|
||||
```
|
||||
.claude-plugin/
|
||||
├── marketplace.json ← Marketplace catalog
|
||||
└── plugins/
|
||||
├── README.md ← This file
|
||||
└── cache-components/
|
||||
├── .claude-plugin/
|
||||
│ └── plugin.json ← Plugin manifest
|
||||
├── skills/
|
||||
│ └── cache-components/
|
||||
│ ├── SKILL.md ← Main skill file
|
||||
│ ├── REFERENCE.md ← API reference
|
||||
│ ├── PATTERNS.md ← Usage patterns
|
||||
│ └── TROUBLESHOOTING.md ← Debugging guide
|
||||
└── README.md ← Plugin documentation
|
||||
```
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"name": "cache-components",
|
||||
"version": "1.0.0",
|
||||
"description": "Expert guidance for Next.js Cache Components and Partial Prerendering (PPR). Proactively activates in projects with cacheComponents: true, providing patterns for 'use cache' directive, cacheLife(), cacheTag(), cache invalidation, and parameter permutation rendering.",
|
||||
"author": {
|
||||
"name": "Next.js Team",
|
||||
"url": "https://nextjs.org"
|
||||
},
|
||||
"homepage": "https://nextjs.org/docs/app/building-your-application/caching",
|
||||
"repository": "https://github.com/vercel/next.js",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"nextjs",
|
||||
"cache",
|
||||
"ppr",
|
||||
"partial-prerendering",
|
||||
"react-server-components",
|
||||
"use-cache"
|
||||
]
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
# Cache Components Plugin for Claude Code
|
||||
|
||||
Expert guidance for Next.js Cache Components and Partial Prerendering (PPR).
|
||||
|
||||
## Features
|
||||
|
||||
This plugin provides a comprehensive skill that:
|
||||
|
||||
- **Proactively activates** in projects with `cacheComponents: true`
|
||||
- Teaches the `'use cache'` directive, `cacheLife()`, `cacheTag()`, and invalidation APIs
|
||||
- Explains **parameter permutation rendering** and subshell generation
|
||||
- Covers migration from deprecated `revalidate`/`dynamic` segment configs
|
||||
- Provides build-time error solutions and debugging guidance
|
||||
|
||||
## Installation
|
||||
|
||||
### Step 1: Add the Next.js Marketplace
|
||||
|
||||
```
|
||||
/plugin marketplace add vercel/next.js
|
||||
```
|
||||
|
||||
### Step 2: Install the Plugin
|
||||
|
||||
```
|
||||
/plugin install cache-components@nextjs
|
||||
```
|
||||
|
||||
Or install via CLI:
|
||||
|
||||
```bash
|
||||
claude plugin install cache-components@nextjs
|
||||
```
|
||||
|
||||
### Step 3 (Optional): Enable for Your Team
|
||||
|
||||
Add to your project's `.claude/settings.json` to auto-enable for all team members:
|
||||
|
||||
```json
|
||||
{
|
||||
"enabledPlugins": {
|
||||
"cache-components@nextjs": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## What's Included
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `SKILL.md` | Core concepts, APIs, and proactive application guidelines |
|
||||
| `REFERENCE.md` | Complete API reference, generateStaticParams, deprecated configs |
|
||||
| `PATTERNS.md` | 12 production patterns including subshell composition |
|
||||
| `TROUBLESHOOTING.md` | Build errors, debugging techniques, common issues |
|
||||
|
||||
## Usage
|
||||
|
||||
Once installed, the skill automatically activates when:
|
||||
|
||||
1. You're working in a Next.js project with `cacheComponents: true`
|
||||
2. You ask about caching, PPR, or the `'use cache'` directive
|
||||
3. You're writing React Server Components or Server Actions
|
||||
|
||||
### Example Triggers
|
||||
|
||||
- "How do I cache this data fetching function?"
|
||||
- "What's the difference between updateTag and revalidateTag?"
|
||||
- "I'm getting a build error about uncached data outside Suspense"
|
||||
- "Help me set up generateStaticParams for my product pages"
|
||||
|
||||
## Key Concepts Covered
|
||||
|
||||
### Parameter Permutation Rendering
|
||||
|
||||
When you provide `generateStaticParams`, Next.js renders ALL permutations:
|
||||
|
||||
```
|
||||
generateStaticParams returns:
|
||||
[{ category: 'jackets', slug: 'bomber' }]
|
||||
|
||||
Next.js renders:
|
||||
/products/jackets/bomber ← Complete page
|
||||
/products/jackets/[slug] ← Category subshell (reusable!)
|
||||
/products/[category]/[slug] ← Fallback shell
|
||||
```
|
||||
|
||||
### Deprecated Segment Configs
|
||||
|
||||
| Old (Deprecated) | New (Cache Components) |
|
||||
|------------------|------------------------|
|
||||
| `export const revalidate = 3600` | `cacheLife('hours')` inside `'use cache'` |
|
||||
| `export const dynamic = 'force-static'` | Use `'use cache'` + Suspense |
|
||||
|
||||
## Contributing
|
||||
|
||||
This plugin lives in the Next.js repository at `.claude-plugin/plugins/cache-components/`.
|
||||
|
||||
To contribute improvements:
|
||||
|
||||
1. Edit files in `.claude-plugin/plugins/cache-components/skills/cache-components/`
|
||||
2. Test locally with `claude --plugin-dir .claude-plugin/plugins/cache-components`
|
||||
3. Submit a PR to the Next.js repository
|
||||
|
||||
## Version History
|
||||
|
||||
### 1.0.0
|
||||
|
||||
- Initial release
|
||||
- Covers `'use cache'`, `cacheLife()`, `cacheTag()`, `updateTag()`, `revalidateTag()`
|
||||
- Parameter permutation rendering and subshell generation
|
||||
- Migration guide from deprecated segment configs
|
||||
- Build-time feedback and troubleshooting
|
||||
@@ -1,832 +0,0 @@
|
||||
# Cache Components Patterns & Recipes
|
||||
|
||||
Common patterns for implementing Cache Components effectively.
|
||||
|
||||
## Pattern 1: Static + Cached + Dynamic Page
|
||||
|
||||
The foundational pattern for Partial Prerendering:
|
||||
|
||||
```tsx
|
||||
import { Suspense } from 'react'
|
||||
import { cacheLife } from 'next/cache'
|
||||
|
||||
// Static - no special handling needed
|
||||
function Header() {
|
||||
return <header>My Blog</header>
|
||||
}
|
||||
|
||||
// Cached - included in static shell
|
||||
async function FeaturedPosts() {
|
||||
'use cache'
|
||||
cacheLife('hours')
|
||||
|
||||
const posts = await db.posts.findMany({
|
||||
where: { featured: true },
|
||||
take: 5,
|
||||
})
|
||||
|
||||
return (
|
||||
<section>
|
||||
{posts.map((post) => (
|
||||
<PostCard key={post.id} post={post} />
|
||||
))}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
// Dynamic - streams at request time
|
||||
async function PersonalizedFeed() {
|
||||
const session = await getSession()
|
||||
const feed = await db.posts.findMany({
|
||||
where: { authorId: { in: session.following } },
|
||||
})
|
||||
|
||||
return <FeedList posts={feed} />
|
||||
}
|
||||
|
||||
// Page composition
|
||||
export default async function HomePage() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<FeaturedPosts />
|
||||
<Suspense fallback={<FeedSkeleton />}>
|
||||
<PersonalizedFeed />
|
||||
</Suspense>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 2: Read-Your-Own-Writes with Server Actions
|
||||
|
||||
Ensure users see their changes immediately:
|
||||
|
||||
```tsx
|
||||
// components/posts.tsx
|
||||
import { cacheTag, cacheLife } from 'next/cache'
|
||||
|
||||
async function PostsList() {
|
||||
'use cache'
|
||||
cacheTag('posts')
|
||||
cacheLife('hours')
|
||||
|
||||
const posts = await db.posts.findMany({ orderBy: { createdAt: 'desc' } })
|
||||
return (
|
||||
<ul>
|
||||
{posts.map((p) => (
|
||||
<PostItem key={p.id} post={p} />
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
// actions/posts.ts
|
||||
'use server'
|
||||
import { updateTag } from 'next/cache'
|
||||
|
||||
export async function createPost(formData: FormData) {
|
||||
const post = await db.posts.create({
|
||||
data: {
|
||||
title: formData.get('title') as string,
|
||||
content: formData.get('content') as string,
|
||||
},
|
||||
})
|
||||
|
||||
// Immediate invalidation - user sees new post right away
|
||||
updateTag('posts')
|
||||
|
||||
return { success: true, postId: post.id }
|
||||
}
|
||||
|
||||
// components/create-post-form.tsx
|
||||
'use client'
|
||||
import { useTransition } from 'react'
|
||||
import { createPost } from '@/actions/posts'
|
||||
|
||||
export function CreatePostForm() {
|
||||
const [isPending, startTransition] = useTransition()
|
||||
|
||||
return (
|
||||
<form
|
||||
action={(formData) => {
|
||||
startTransition(() => createPost(formData))
|
||||
}}
|
||||
>
|
||||
<input name="title" required />
|
||||
<textarea name="content" required />
|
||||
<button disabled={isPending}>
|
||||
{isPending ? 'Creating...' : 'Create Post'}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 3: Granular Cache Invalidation
|
||||
|
||||
Tag caches at multiple levels for precise invalidation:
|
||||
|
||||
```tsx
|
||||
// Cached with multiple tags
|
||||
async function BlogPost({ postId }: { postId: string }) {
|
||||
'use cache'
|
||||
cacheTag('posts', `post-${postId}`)
|
||||
cacheLife('days')
|
||||
|
||||
const post = await db.posts.findUnique({
|
||||
where: { id: postId },
|
||||
include: { author: true, comments: true },
|
||||
})
|
||||
|
||||
return <Article post={post} />
|
||||
}
|
||||
|
||||
async function AuthorPosts({ authorId }: { authorId: string }) {
|
||||
'use cache'
|
||||
cacheTag('posts', `author-${authorId}`)
|
||||
cacheLife('hours')
|
||||
|
||||
const posts = await db.posts.findMany({
|
||||
where: { authorId },
|
||||
})
|
||||
|
||||
return <PostGrid posts={posts} />
|
||||
}
|
||||
|
||||
// Server actions with targeted invalidation
|
||||
'use server'
|
||||
import { updateTag } from 'next/cache'
|
||||
|
||||
export async function updatePost(postId: string, data: FormData) {
|
||||
const post = await db.posts.update({
|
||||
where: { id: postId },
|
||||
data: { title: data.get('title'), content: data.get('content') },
|
||||
})
|
||||
|
||||
// Invalidate specific post only
|
||||
updateTag(`post-${postId}`)
|
||||
}
|
||||
|
||||
export async function deleteAuthorPosts(authorId: string) {
|
||||
await db.posts.deleteMany({ where: { authorId } })
|
||||
|
||||
// Invalidate all author's posts
|
||||
updateTag(`author-${authorId}`)
|
||||
}
|
||||
|
||||
export async function clearAllPosts() {
|
||||
await db.posts.deleteMany()
|
||||
|
||||
// Nuclear option - invalidate everything tagged 'posts'
|
||||
updateTag('posts')
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 4: Cached Data Fetching Functions
|
||||
|
||||
Create reusable cached data fetchers:
|
||||
|
||||
```tsx
|
||||
// lib/data.ts
|
||||
import { cacheTag, cacheLife } from 'next/cache'
|
||||
|
||||
export async function getUser(userId: string) {
|
||||
'use cache'
|
||||
cacheTag('users', `user-${userId}`)
|
||||
cacheLife('hours')
|
||||
|
||||
return db.users.findUnique({ where: { id: userId } })
|
||||
}
|
||||
|
||||
export async function getPostsByCategory(category: string) {
|
||||
'use cache'
|
||||
cacheTag('posts', `category-${category}`)
|
||||
cacheLife('minutes')
|
||||
|
||||
return db.posts.findMany({
|
||||
where: { category },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
}
|
||||
|
||||
export async function getPopularProducts() {
|
||||
'use cache'
|
||||
cacheTag('products', 'popular')
|
||||
cacheLife('hours')
|
||||
|
||||
return db.products.findMany({
|
||||
orderBy: { salesCount: 'desc' },
|
||||
take: 10,
|
||||
})
|
||||
}
|
||||
|
||||
// Usage in components
|
||||
async function Sidebar() {
|
||||
const popular = await getPopularProducts()
|
||||
return <ProductList products={popular} />
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 5: Stale-While-Revalidate for Background Updates
|
||||
|
||||
Use `revalidateTag` for non-critical updates:
|
||||
|
||||
```tsx
|
||||
// For background analytics or non-user-facing updates
|
||||
'use server'
|
||||
import { revalidateTag } from 'next/cache'
|
||||
|
||||
export async function trackView(postId: string) {
|
||||
await db.posts.update({
|
||||
where: { id: postId },
|
||||
data: { views: { increment: 1 } },
|
||||
})
|
||||
|
||||
// Background revalidation - old count shown while updating
|
||||
revalidateTag(`post-${postId}`, 'max')
|
||||
}
|
||||
|
||||
// For user-facing mutations, use updateTag instead
|
||||
export async function likePost(postId: string) {
|
||||
await db.likes.create({ data: { postId, userId: getCurrentUserId() } })
|
||||
|
||||
// Immediate - user sees their like right away
|
||||
updateTag(`post-${postId}`)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 6: Conditional Caching Based on Content
|
||||
|
||||
Cache based on content characteristics:
|
||||
|
||||
```tsx
|
||||
async function ContentBlock({ id }: { id: string }) {
|
||||
'use cache'
|
||||
|
||||
const content = await db.content.findUnique({ where: { id } })
|
||||
|
||||
// Adjust cache life based on content type
|
||||
if (content.type === 'static') {
|
||||
cacheLife('max')
|
||||
cacheTag('static-content')
|
||||
} else if (content.type === 'news') {
|
||||
cacheLife('minutes')
|
||||
cacheTag('news', `news-${id}`)
|
||||
} else {
|
||||
cacheLife('default')
|
||||
cacheTag('content', `content-${id}`)
|
||||
}
|
||||
|
||||
return <ContentRenderer content={content} />
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 7: Nested Cached Components
|
||||
|
||||
Compose cached components for fine-grained caching:
|
||||
|
||||
```tsx
|
||||
// Each component caches independently
|
||||
async function Header() {
|
||||
'use cache'
|
||||
cacheTag('layout', 'header')
|
||||
cacheLife('days')
|
||||
|
||||
const nav = await db.navigation.findFirst()
|
||||
return <Nav items={nav.items} />
|
||||
}
|
||||
|
||||
async function Footer() {
|
||||
'use cache'
|
||||
cacheTag('layout', 'footer')
|
||||
cacheLife('days')
|
||||
|
||||
const footer = await db.footer.findFirst()
|
||||
return <FooterContent data={footer} />
|
||||
}
|
||||
|
||||
async function Sidebar({ category }: { category: string }) {
|
||||
'use cache'
|
||||
cacheTag('sidebar', `category-${category}`)
|
||||
cacheLife('hours')
|
||||
|
||||
const related = await db.posts.findMany({
|
||||
where: { category },
|
||||
take: 5,
|
||||
})
|
||||
return <RelatedPosts posts={related} />
|
||||
}
|
||||
|
||||
// Page composes cached components
|
||||
export default async function BlogLayout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
params: { category: string }
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main>
|
||||
{children}
|
||||
<Sidebar category={params.category} />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 8: E-commerce Product Page
|
||||
|
||||
Complete example for e-commerce:
|
||||
|
||||
```tsx
|
||||
// app/products/[id]/page.tsx
|
||||
import { Suspense } from 'react'
|
||||
import { cacheTag, cacheLife } from 'next/cache'
|
||||
|
||||
// Cached product details (changes rarely)
|
||||
async function ProductDetails({ productId }: { productId: string }) {
|
||||
'use cache'
|
||||
cacheTag('products', `product-${productId}`)
|
||||
cacheLife('hours')
|
||||
|
||||
const product = await db.products.findUnique({
|
||||
where: { id: productId },
|
||||
include: { images: true, specifications: true },
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ProductGallery images={product.images} />
|
||||
<ProductInfo product={product} />
|
||||
<Specifications specs={product.specifications} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Cached reviews (moderate change frequency)
|
||||
async function ProductReviews({ productId }: { productId: string }) {
|
||||
'use cache'
|
||||
cacheTag(`product-${productId}-reviews`)
|
||||
cacheLife('minutes')
|
||||
|
||||
const reviews = await db.reviews.findMany({
|
||||
where: { productId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 10,
|
||||
})
|
||||
|
||||
return <ReviewsList reviews={reviews} />
|
||||
}
|
||||
|
||||
// Dynamic inventory (real-time)
|
||||
async function InventoryStatus({ productId }: { productId: string }) {
|
||||
// No cache - always fresh
|
||||
const inventory = await db.inventory.findUnique({
|
||||
where: { productId },
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
{inventory.quantity > 0 ? (
|
||||
<span className="text-green-600">In Stock ({inventory.quantity})</span>
|
||||
) : (
|
||||
<span className="text-red-600">Out of Stock</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Page composition
|
||||
export default async function ProductPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
}) {
|
||||
const { id } = await params
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProductDetails productId={id} />
|
||||
|
||||
<Suspense fallback={<InventorySkeleton />}>
|
||||
<InventoryStatus productId={id} />
|
||||
</Suspense>
|
||||
|
||||
{/* Suspense around cached components:
|
||||
- At BUILD TIME (PPR): Cached content is pre-rendered into the static shell,
|
||||
so the fallback is never shown for initial page loads.
|
||||
- At RUNTIME (cache miss/expiration): When the cache expires or on cold start,
|
||||
Suspense shows the fallback while fresh data loads.
|
||||
- For long-lived caches ('minutes', 'hours', 'days'), Suspense is optional
|
||||
but improves UX during the rare cache miss. */}
|
||||
<Suspense fallback={<ReviewsSkeleton />}>
|
||||
<ProductReviews productId={id} />
|
||||
</Suspense>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 9: Multi-tenant SaaS Application
|
||||
|
||||
Handle tenant-specific caching:
|
||||
|
||||
```tsx
|
||||
// lib/tenant.ts
|
||||
export async function getTenantId() {
|
||||
const host = (await headers()).get('host')
|
||||
return host?.split('.')[0] // subdomain as tenant ID
|
||||
}
|
||||
|
||||
// Tenant-scoped cached data
|
||||
async function TenantDashboard({ tenantId }: { tenantId: string }) {
|
||||
'use cache'
|
||||
cacheTag(`tenant-${tenantId}`, 'dashboards')
|
||||
cacheLife('minutes')
|
||||
|
||||
const data = await db.dashboards.findFirst({
|
||||
where: { tenantId },
|
||||
})
|
||||
|
||||
return <Dashboard data={data} />
|
||||
}
|
||||
|
||||
// Page with tenant context
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<Suspense fallback={<DashboardSkeleton />}>
|
||||
<DashboardLoader />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
async function DashboardLoader() {
|
||||
const tenantId = await getTenantId()
|
||||
return <TenantDashboard tenantId={tenantId} />
|
||||
}
|
||||
|
||||
// Tenant-specific invalidation
|
||||
'use server'
|
||||
import { updateTag } from 'next/cache'
|
||||
|
||||
export async function updateTenantSettings(data: FormData) {
|
||||
const tenantId = await getTenantId()
|
||||
|
||||
await db.settings.update({
|
||||
where: { tenantId },
|
||||
data: {
|
||||
/* ... */
|
||||
},
|
||||
})
|
||||
|
||||
// Only invalidate this tenant's cache
|
||||
updateTag(`tenant-${tenantId}`)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 10: Subshell Composition with generateStaticParams
|
||||
|
||||
Leverage parameter permutations to create reusable subshells:
|
||||
|
||||
```tsx
|
||||
// app/products/[category]/[slug]/page.tsx
|
||||
import { Suspense } from 'react'
|
||||
import { cacheLife, cacheTag } from 'next/cache'
|
||||
|
||||
// Product details - uses both params
|
||||
async function ProductDetails({
|
||||
category,
|
||||
slug,
|
||||
}: {
|
||||
category: string
|
||||
slug: string
|
||||
}) {
|
||||
'use cache'
|
||||
cacheTag('products', `product-${slug}`)
|
||||
cacheLife('hours')
|
||||
|
||||
const product = await db.products.findUnique({
|
||||
where: { category, slug },
|
||||
})
|
||||
|
||||
return <ProductCard product={product} />
|
||||
}
|
||||
|
||||
export default async function ProductPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ category: string; slug: string }>
|
||||
}) {
|
||||
const { category, slug } = await params
|
||||
|
||||
return <ProductDetails category={category} slug={slug} />
|
||||
}
|
||||
|
||||
// Provide params to enable subshell generation
|
||||
export async function generateStaticParams() {
|
||||
const products = await db.products.findMany({
|
||||
select: { category: true, slug: true },
|
||||
take: 100,
|
||||
})
|
||||
return products.map(({ category, slug }) => ({ category, slug }))
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// app/products/[category]/layout.tsx
|
||||
import { Suspense } from 'react'
|
||||
import { cacheLife, cacheTag } from 'next/cache'
|
||||
|
||||
// Category header - uses only category param
|
||||
async function CategoryHeader({ category }: { category: string }) {
|
||||
'use cache'
|
||||
cacheTag('categories', `category-${category}`)
|
||||
cacheLife('days')
|
||||
|
||||
const cat = await db.categories.findUnique({ where: { slug: category } })
|
||||
return (
|
||||
<header>
|
||||
<h1>{cat.name}</h1>
|
||||
<p>{cat.description}</p>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export default async function CategoryLayout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
params: Promise<{ category: string }>
|
||||
}) {
|
||||
const { category } = await params
|
||||
|
||||
return (
|
||||
<>
|
||||
<CategoryHeader category={category} />
|
||||
{/* Suspense enables subshell generation */}
|
||||
<Suspense fallback={<ProductSkeleton />}>{children}</Suspense>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Result**: When users navigate to `/products/jackets/unknown-jacket`:
|
||||
|
||||
1. Category subshell (`/products/jackets/[slug]`) served instantly
|
||||
2. Product details stream in as they load
|
||||
3. Future visits to any jacket product reuse the category shell
|
||||
|
||||
---
|
||||
|
||||
## Pattern 11: Hierarchical Params for Deep Routes
|
||||
|
||||
For deeply nested routes, structure layouts to maximize subshell reuse:
|
||||
|
||||
```tsx
|
||||
// Route: /store/[region]/[category]/[productId]
|
||||
|
||||
// app/store/[region]/layout.tsx
|
||||
export default async function RegionLayout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
params: Promise<{ region: string }>
|
||||
}) {
|
||||
const { region } = await params
|
||||
|
||||
return (
|
||||
<>
|
||||
<RegionHeader region={region} /> {/* Cached */}
|
||||
<RegionPromos region={region} /> {/* Cached */}
|
||||
<Suspense>{children}</Suspense> {/* Subshell boundary */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// app/store/[region]/[category]/layout.tsx
|
||||
export default async function CategoryLayout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
params: Promise<{ region: string; category: string }>
|
||||
}) {
|
||||
const { region, category } = await params
|
||||
|
||||
return (
|
||||
<>
|
||||
<CategoryNav region={region} category={category} /> {/* Cached */}
|
||||
<Suspense>{children}</Suspense> {/* Subshell boundary */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// app/store/[region]/[category]/[productId]/page.tsx
|
||||
export default async function ProductPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ region: string; category: string; productId: string }>
|
||||
}) {
|
||||
const { region, category, productId } = await params
|
||||
|
||||
return <ProductDetails region={region} productId={productId} />
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
// Return popular products - subshells generated for all unique region/category combos
|
||||
return [
|
||||
{ region: 'us', category: 'electronics', productId: 'iphone-16' },
|
||||
{ region: 'us', category: 'electronics', productId: 'macbook-pro' },
|
||||
{ region: 'us', category: 'clothing', productId: 'hoodie-xl' },
|
||||
{ region: 'eu', category: 'electronics', productId: 'iphone-16' },
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Generated subshells:**
|
||||
|
||||
- `/store/us/[category]/[productId]` - US region shell
|
||||
- `/store/eu/[category]/[productId]` - EU region shell
|
||||
- `/store/us/electronics/[productId]` - US Electronics shell
|
||||
- `/store/us/clothing/[productId]` - US Clothing shell
|
||||
- `/store/eu/electronics/[productId]` - EU Electronics shell
|
||||
|
||||
---
|
||||
|
||||
## When to Use Suspense with Cached Components
|
||||
|
||||
Understanding when Suspense is required vs. optional for cached components:
|
||||
|
||||
### Dynamic Components (no cache) → Suspense Required
|
||||
|
||||
```tsx
|
||||
// Dynamic content MUST have Suspense for streaming
|
||||
async function PersonalizedFeed() {
|
||||
const session = await getSession() // Dynamic - reads cookies
|
||||
const feed = await fetchFeed(session.userId)
|
||||
return <Feed posts={feed} />
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Suspense fallback={<FeedSkeleton />}>
|
||||
<PersonalizedFeed />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Cached Components → Suspense Optional (but recommended)
|
||||
|
||||
```tsx
|
||||
// Cached content: Suspense is optional but improves UX
|
||||
async function ProductReviews({ productId }: { productId: string }) {
|
||||
'use cache'
|
||||
cacheLife('minutes')
|
||||
const reviews = await fetchReviews(productId)
|
||||
return <ReviewsList reviews={reviews} />
|
||||
}
|
||||
|
||||
// ✅ With Suspense - handles cache miss gracefully
|
||||
<Suspense fallback={<ReviewsSkeleton />}>
|
||||
<ProductReviews productId={id} />
|
||||
</Suspense>
|
||||
|
||||
// ✅ Without Suspense - also valid for long-lived caches
|
||||
<ProductReviews productId={id} />
|
||||
```
|
||||
|
||||
### Why Cached Components Don't Always Need Suspense
|
||||
|
||||
| Scenario | What Happens | Suspense Needed? |
|
||||
|----------|--------------|------------------|
|
||||
| **Build time (PPR enabled)** | Content pre-rendered into static shell | No - fallback never shown |
|
||||
| **Runtime - cache hit** | Cached result returned immediately | No - no suspension |
|
||||
| **Runtime - cache miss** | Async function executes, component suspends | Yes - for better UX |
|
||||
|
||||
### Recommendations by Cache Lifetime
|
||||
|
||||
| Cache Lifetime | Suspense Recommendation | Reasoning |
|
||||
|----------------|------------------------|-----------|
|
||||
| `'seconds'` | **Recommended** | Frequent cache misses |
|
||||
| `'minutes'` | Optional | ~5 min expiry, occasional misses |
|
||||
| `'hours'` / `'days'` | Optional | Rare cache misses |
|
||||
| `'max'` | Not needed | Essentially static |
|
||||
|
||||
### The Trade-off
|
||||
|
||||
**Without Suspense**: On cache miss, the page waits for data before rendering anything downstream. For long-lived caches, this is rare and brief.
|
||||
|
||||
**With Suspense**: On cache miss, users see the skeleton immediately while data loads. Better perceived performance, slightly more code.
|
||||
|
||||
**Rule of thumb**: When in doubt, add Suspense. It never hurts and handles edge cases gracefully.
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
### ❌ Caching user-specific data without parameters
|
||||
|
||||
```tsx
|
||||
// BAD: Same cache for all users
|
||||
async function UserProfile() {
|
||||
'use cache'
|
||||
const user = await getCurrentUser() // Different per user!
|
||||
return <Profile user={user} />
|
||||
}
|
||||
|
||||
// GOOD: User ID as parameter (becomes cache key)
|
||||
async function UserProfile({ userId }: { userId: string }) {
|
||||
'use cache'
|
||||
cacheTag(`user-${userId}`)
|
||||
const user = await db.users.findUnique({ where: { id: userId } })
|
||||
return <Profile user={user} />
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ Over-caching volatile data
|
||||
|
||||
```tsx
|
||||
// BAD: Caching real-time data
|
||||
async function StockPrice({ symbol }: { symbol: string }) {
|
||||
'use cache'
|
||||
cacheLife('hours') // Stale prices!
|
||||
return await fetchStockPrice(symbol)
|
||||
}
|
||||
|
||||
// GOOD: Don't cache, or use very short cache
|
||||
async function StockPrice({ symbol }: { symbol: string }) {
|
||||
'use cache'
|
||||
cacheLife('seconds') // 1 second max
|
||||
return await fetchStockPrice(symbol)
|
||||
}
|
||||
|
||||
// BETTER: No cache for truly real-time
|
||||
async function StockPrice({ symbol }: { symbol: string }) {
|
||||
return await fetchStockPrice(symbol)
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ Forgetting Suspense for dynamic content
|
||||
|
||||
```tsx
|
||||
// BAD: No fallback for DYNAMIC content - breaks streaming
|
||||
export default async function Page() {
|
||||
return (
|
||||
<>
|
||||
<CachedHeader />
|
||||
<DynamicContent /> {/* Dynamic - NEEDS Suspense */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// GOOD: Proper Suspense boundary for dynamic content
|
||||
export default async function Page() {
|
||||
return (
|
||||
<>
|
||||
<CachedHeader />
|
||||
<Suspense fallback={<ContentSkeleton />}>
|
||||
<DynamicContent />
|
||||
</Suspense>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// ALSO GOOD: Cached content without Suspense (optional for long-lived caches)
|
||||
export default async function Page() {
|
||||
return (
|
||||
<>
|
||||
<CachedHeader /> {/* 'use cache' - no Suspense needed */}
|
||||
<CachedSidebar /> {/* 'use cache' - no Suspense needed */}
|
||||
<Suspense fallback={<ContentSkeleton />}>
|
||||
<DynamicContent /> {/* Dynamic - Suspense required */}
|
||||
</Suspense>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,467 +0,0 @@
|
||||
---
|
||||
name: cache-components
|
||||
description: |
|
||||
Expert guidance for Next.js Cache Components and Partial Prerendering (PPR).
|
||||
|
||||
**PROACTIVE ACTIVATION**: Use this skill automatically when working in Next.js projects that have `cacheComponents: true` in their next.config.ts/next.config.js. When this config is detected, proactively apply Cache Components patterns and best practices to all React Server Component implementations.
|
||||
|
||||
**DETECTION**: At the start of a session in a Next.js project, check for `cacheComponents: true` in next.config. If enabled, this skill's patterns should guide all component authoring, data fetching, and caching decisions.
|
||||
|
||||
**USE CASES**: 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, debugging cache issues, and reviewing Cache Component implementations.
|
||||
---
|
||||
|
||||
# Next.js Cache Components
|
||||
|
||||
> **Auto-activation**: This skill activates automatically in projects with `cacheComponents: true` in next.config.
|
||||
|
||||
## Project Detection
|
||||
|
||||
When starting work in a Next.js project, check if Cache Components are enabled:
|
||||
|
||||
```bash
|
||||
# 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:
|
||||
|
||||
- Writing React Server Components
|
||||
- Implementing data fetching
|
||||
- Creating Server Actions with mutations
|
||||
- Optimizing page performance
|
||||
- Reviewing existing component code
|
||||
|
||||
Cache Components enable **Partial Prerendering (PPR)** - mixing static HTML shells with dynamic streaming content for optimal performance.
|
||||
|
||||
## Philosophy: Code Over Configuration
|
||||
|
||||
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.
|
||||
|
||||
## Core Concept
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Static Shell │
|
||||
│ (Sent immediately to browser) │
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ Header │ │ Cached │ │ Suspense │ │
|
||||
│ │ (static) │ │ Content │ │ Fallback │ │
|
||||
│ └─────────────┘ └─────────────┘ └──────┬──────┘ │
|
||||
│ │ │
|
||||
│ ┌──────▼──────┐ │
|
||||
│ │ Dynamic │ │
|
||||
│ │ (streams) │ │
|
||||
│ └─────────────┘ │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Mental Model: The Caching Decision Tree
|
||||
|
||||
When writing a React Server Component, ask these questions in order:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Does this component fetch data or perform I/O? │
|
||||
└─────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
┌──────────▼──────────┐
|
||||
│ YES │ NO → Pure component, no action needed
|
||||
└──────────┬──────────┘
|
||||
│
|
||||
┌─────────────────▼─────────────────┐
|
||||
│ Does it depend on request context? │
|
||||
│ (cookies, headers, searchParams) │
|
||||
└─────────────────┬─────────────────┘
|
||||
│
|
||||
┌────────────┴────────────┐
|
||||
│ │
|
||||
┌────▼────┐ ┌─────▼─────┐
|
||||
│ YES │ │ NO │
|
||||
└────┬────┘ └─────┬─────┘
|
||||
│ │
|
||||
│ ┌─────▼─────────────────┐
|
||||
│ │ Can this be cached? │
|
||||
│ │ (same for all users?) │
|
||||
│ └─────┬─────────────────┘
|
||||
│ │
|
||||
│ ┌──────────┴──────────┐
|
||||
│ │ │
|
||||
│ ┌────▼────┐ ┌─────▼─────┐
|
||||
│ │ YES │ │ NO │
|
||||
│ └────┬────┘ └─────┬─────┘
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 'use cache' │
|
||||
│ + cacheTag() │
|
||||
│ + cacheLife() │
|
||||
│ │
|
||||
└──────────────┬─────────────────────┘
|
||||
│
|
||||
▼
|
||||
Wrap in <Suspense>
|
||||
(dynamic streaming)
|
||||
```
|
||||
|
||||
**Key insight**: The `'use cache'` directive is for data that's the _same across users_. User-specific data stays dynamic with Suspense.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Enable Cache Components
|
||||
|
||||
```typescript
|
||||
// next.config.ts
|
||||
import type { NextConfig } from 'next'
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
cacheComponents: true,
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
```
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```tsx
|
||||
// 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Core APIs
|
||||
|
||||
### 1. `'use cache'` Directive
|
||||
|
||||
Marks code as cacheable. Can be applied at three levels:
|
||||
|
||||
```tsx
|
||||
// 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`.
|
||||
|
||||
### 2. `cacheLife()` - Control Cache Duration
|
||||
|
||||
```tsx
|
||||
import { 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'`
|
||||
|
||||
### 3. `cacheTag()` - Tag for Invalidation
|
||||
|
||||
```tsx
|
||||
import { 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 } })
|
||||
}
|
||||
```
|
||||
|
||||
### 4. `updateTag()` - Immediate Invalidation
|
||||
|
||||
For **read-your-own-writes** semantics:
|
||||
|
||||
```tsx
|
||||
'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
|
||||
}
|
||||
```
|
||||
|
||||
### 5. `revalidateTag()` - Background Revalidation
|
||||
|
||||
For stale-while-revalidate pattern:
|
||||
|
||||
```tsx
|
||||
'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
|
||||
}
|
||||
```
|
||||
|
||||
## When to Use Each Pattern
|
||||
|
||||
| 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 |
|
||||
|
||||
## Parameter Permutations & Subshells
|
||||
|
||||
**Critical Concept**: With Cache Components, Next.js renders ALL permutations of provided parameters to create reusable subshells.
|
||||
|
||||
```tsx
|
||||
// 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` Requirements
|
||||
|
||||
With Cache Components enabled:
|
||||
|
||||
1. **Must provide at least one parameter** - Empty arrays now cause build errors (prevents silent production failures)
|
||||
2. **Params prove static safety** - Providing params lets Next.js verify no dynamic APIs are called
|
||||
3. **Partial params create subshells** - Each unique permutation generates a reusable shell
|
||||
|
||||
```tsx
|
||||
// ❌ 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 }))
|
||||
}
|
||||
```
|
||||
|
||||
## Cache Key = Arguments
|
||||
|
||||
Arguments become part of the cache key:
|
||||
|
||||
```tsx
|
||||
// Different userId = different cache entry
|
||||
async function UserData({ userId }: { userId: string }) {
|
||||
'use cache'
|
||||
cacheTag(`user-${userId}`)
|
||||
|
||||
return await fetchUser(userId)
|
||||
}
|
||||
```
|
||||
|
||||
## Build-Time Feedback
|
||||
|
||||
Cache Components provides early feedback during development. These build errors **guide you toward optimal patterns**:
|
||||
|
||||
### Error: Dynamic data outside Suspense
|
||||
|
||||
```
|
||||
Error: Accessing cookies/headers/searchParams outside a Suspense boundary
|
||||
```
|
||||
|
||||
**Solution**: Wrap dynamic components in `<Suspense>`:
|
||||
|
||||
```tsx
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
<ComponentThatUsesCookies />
|
||||
</Suspense>
|
||||
```
|
||||
|
||||
### Error: Uncached data outside Suspense
|
||||
|
||||
```
|
||||
Error: Accessing uncached data outside Suspense
|
||||
```
|
||||
|
||||
**Solution**: Either cache the data or wrap in Suspense:
|
||||
|
||||
```tsx
|
||||
// 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: Request data inside cache
|
||||
|
||||
```
|
||||
Error: Cannot access cookies/headers inside 'use cache'
|
||||
```
|
||||
|
||||
**Solution**: Extract runtime data outside cache boundary (see "Handling Runtime Data" above).
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- For complete API reference, see [REFERENCE.md](REFERENCE.md)
|
||||
- For common patterns and recipes, see [PATTERNS.md](PATTERNS.md)
|
||||
- For debugging and troubleshooting, see [TROUBLESHOOTING.md](TROUBLESHOOTING.md)
|
||||
|
||||
## Code Generation Guidelines
|
||||
|
||||
When generating Cache Component code:
|
||||
|
||||
1. **Always use `async`** - All cached functions must be async
|
||||
2. **Place `'use cache'` first** - Must be first statement in function body
|
||||
3. **Call `cacheLife()` early** - Should follow `'use cache'` directive
|
||||
4. **Tag meaningfully** - Use semantic tags that match your invalidation needs
|
||||
5. **Extract runtime data** - Move `cookies()`/`headers()` outside cached scope
|
||||
6. **Wrap dynamic content** - Use `<Suspense>` for non-cached async components
|
||||
|
||||
---
|
||||
|
||||
## Proactive Application (When Cache Components Enabled)
|
||||
|
||||
When `cacheComponents: true` is detected in the project, **automatically apply these patterns**:
|
||||
|
||||
### When Writing Data Fetching Components
|
||||
|
||||
Ask yourself: "Can this data be cached?" If yes, add `'use cache'`:
|
||||
|
||||
```tsx
|
||||
// Before: Uncached fetch
|
||||
async function ProductList() {
|
||||
const products = await db.products.findMany()
|
||||
return <Grid products={products} />
|
||||
}
|
||||
|
||||
// After: With caching
|
||||
async function ProductList() {
|
||||
'use cache'
|
||||
cacheTag('products')
|
||||
cacheLife('hours')
|
||||
|
||||
const products = await db.products.findMany()
|
||||
return <Grid products={products} />
|
||||
}
|
||||
```
|
||||
|
||||
### When Writing Server Actions
|
||||
|
||||
Always invalidate relevant caches after mutations:
|
||||
|
||||
```tsx
|
||||
'use server'
|
||||
import { updateTag } from 'next/cache'
|
||||
|
||||
export async function createProduct(data: FormData) {
|
||||
await db.products.create({ data })
|
||||
updateTag('products') // Don't forget!
|
||||
}
|
||||
```
|
||||
|
||||
### When Composing Pages
|
||||
|
||||
Structure with static shell + cached content + dynamic streaming:
|
||||
|
||||
```tsx
|
||||
export default async function Page() {
|
||||
return (
|
||||
<>
|
||||
<StaticHeader /> {/* No cache needed */}
|
||||
<CachedContent /> {/* 'use cache' */}
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
<DynamicUserContent /> {/* Streams at runtime */}
|
||||
</Suspense>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### When Reviewing Code
|
||||
|
||||
Flag these issues in Cache Components projects:
|
||||
|
||||
- [ ] Data fetching without `'use cache'` where caching would benefit
|
||||
- [ ] Missing `cacheTag()` calls (makes invalidation impossible)
|
||||
- [ ] Missing `cacheLife()` (relies on defaults which may not be appropriate)
|
||||
- [ ] Server Actions without `updateTag()`/`revalidateTag()` after mutations
|
||||
- [ ] `cookies()`/`headers()` called inside `'use cache'` scope
|
||||
- [ ] Dynamic components without `<Suspense>` boundaries
|
||||
- [ ] **DEPRECATED**: `export const revalidate` - replace with `cacheLife()` in `'use cache'`
|
||||
- [ ] **DEPRECATED**: `export const dynamic` - replace with Suspense + cache boundaries
|
||||
- [ ] Empty `generateStaticParams()` return - must provide at least one param
|
||||
@@ -1,719 +0,0 @@
|
||||
# Cache Components Troubleshooting
|
||||
|
||||
Common issues, debugging techniques, and solutions for Cache Components.
|
||||
|
||||
## Build-Time Feedback Philosophy
|
||||
|
||||
Cache Components introduces **early feedback** during development. Unlike before where errors might only appear in production, Cache Components produces build errors that **guide you toward optimal patterns**.
|
||||
|
||||
Key principle: **If it builds, it's correct.** The build process validates that:
|
||||
|
||||
- Dynamic data isn't accessed outside Suspense boundaries
|
||||
- Cached data doesn't depend on request-specific APIs
|
||||
- `generateStaticParams` provides valid parameters to test rendering
|
||||
|
||||
---
|
||||
|
||||
## Quick Debugging Checklist
|
||||
|
||||
Copy this checklist when debugging cache issues:
|
||||
|
||||
### Cache Not Working
|
||||
|
||||
- [ ] `cacheComponents: true` in next.config?
|
||||
- [ ] Function is `async`?
|
||||
- [ ] `'use cache'` is FIRST statement in function body?
|
||||
- [ ] All arguments are serializable (no functions, class instances)?
|
||||
- [ ] Not accessing `cookies()`/`headers()` inside cache?
|
||||
|
||||
### Stale Data After Mutation
|
||||
|
||||
- [ ] Called `updateTag()` or `revalidateTag()` after mutation?
|
||||
- [ ] Tag in invalidation matches tag in `cacheTag()`?
|
||||
- [ ] Using `updateTag()` (not `revalidateTag()`) for immediate updates?
|
||||
|
||||
### Build Errors
|
||||
|
||||
- [ ] Dynamic data wrapped in `<Suspense>`?
|
||||
- [ ] `generateStaticParams` returns at least one param?
|
||||
- [ ] Not mixing `'use cache'` with `cookies()`/`headers()`?
|
||||
|
||||
### Performance Issues
|
||||
|
||||
- [ ] Cache granularity appropriate? (not too coarse/fine)
|
||||
- [ ] `cacheLife` set appropriately for data volatility?
|
||||
- [ ] Using hierarchical tags for targeted invalidation?
|
||||
|
||||
---
|
||||
|
||||
## Error: UseCacheTimeoutError
|
||||
|
||||
### Symptoms
|
||||
|
||||
```
|
||||
Error: A component used 'use cache' but didn't complete within 50 seconds.
|
||||
```
|
||||
|
||||
### Cause
|
||||
|
||||
The cached function is accessing request-specific data (cookies, headers, searchParams) or making requests that depend on runtime context.
|
||||
|
||||
### Solution
|
||||
|
||||
User-specific content that depends on runtime data (cookies, headers, searchParams) should **not be cached**. Instead, stream it dynamically:
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG: Trying to cache user-specific content
|
||||
async function UserContent() {
|
||||
'use cache'
|
||||
const session = await cookies() // Causes timeout!
|
||||
return await fetchContent(session.userId)
|
||||
}
|
||||
|
||||
// ✅ CORRECT: Don't cache user-specific content, stream it instead
|
||||
async function UserContent() {
|
||||
const session = await cookies()
|
||||
return await fetchContent(session.get('userId')?.value)
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<UserContent /> {/* No 'use cache' - streams dynamically */}
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Key insight**: Cache Components are for content that can be shared across users (e.g., product details, blog posts). User-specific content should stream at request time.
|
||||
|
||||
---
|
||||
|
||||
## Error: Cannot use 'use cache' with sync function
|
||||
|
||||
### Symptoms
|
||||
|
||||
```
|
||||
Error: 'use cache' can only be used in async functions
|
||||
```
|
||||
|
||||
### Cause
|
||||
|
||||
Cache Components require async functions because cached outputs are streamed.
|
||||
|
||||
### Solution
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG: Synchronous function
|
||||
function CachedComponent() {
|
||||
'use cache'
|
||||
return <div>Hello</div>
|
||||
}
|
||||
|
||||
// ✅ CORRECT: Async function
|
||||
async function CachedComponent() {
|
||||
'use cache'
|
||||
return <div>Hello</div>
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error: Dynamic Data Outside Suspense
|
||||
|
||||
### Symptoms
|
||||
|
||||
```
|
||||
Error: Accessing cookies/headers/searchParams outside a Suspense boundary
|
||||
```
|
||||
|
||||
### Cause
|
||||
|
||||
With Cache Components, accessing request-specific APIs (cookies, headers, searchParams, connection) requires a Suspense boundary so Next.js can provide a static fallback.
|
||||
|
||||
### Why This Changed
|
||||
|
||||
**Before Cache Components**: The page silently became fully dynamic - no static content served.
|
||||
|
||||
**After Cache Components**: Build error ensures you explicitly handle the dynamic boundary.
|
||||
|
||||
### Solution
|
||||
|
||||
Wrap dynamic content in Suspense:
|
||||
|
||||
```tsx
|
||||
// ❌ ERROR: No Suspense boundary
|
||||
export default async function Page() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<UserDeals /> {/* Uses cookies() */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// ✅ CORRECT: Suspense provides static fallback
|
||||
export default async function Page() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Suspense fallback={<DealsSkeleton />}>
|
||||
<UserDeals />
|
||||
</Suspense>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
> **See also**: Pattern 1 (Static + Cached + Dynamic Page) in PATTERNS.md shows the foundational Suspense boundary pattern.
|
||||
|
||||
---
|
||||
|
||||
## Error: Uncached Data Outside Suspense
|
||||
|
||||
### Symptoms
|
||||
|
||||
```
|
||||
Error: Accessing uncached data outside Suspense
|
||||
```
|
||||
|
||||
### Cause
|
||||
|
||||
With Cache Components, ALL **async** I/O is considered dynamic by default. Database queries, fetch calls, and file reads must either be cached or wrapped in Suspense.
|
||||
|
||||
> **Note on synchronous databases**: Libraries with synchronous APIs (e.g., `better-sqlite3`) don't trigger this error because they don't involve async I/O. Synchronous operations complete during render and are included in the static shell. However, this also means they block the render thread - use judiciously for small, fast queries only.
|
||||
|
||||
### Solution
|
||||
|
||||
Either cache the data or wrap in Suspense:
|
||||
|
||||
```tsx
|
||||
// ❌ ERROR: Uncached database query without Suspense
|
||||
export default async function ProductPage({ params }) {
|
||||
const product = await db.products.findUnique({ where: { id: params.id } })
|
||||
return <ProductCard product={product} />
|
||||
}
|
||||
|
||||
// ✅ OPTION 1: Cache the data
|
||||
async function getProduct(id: string) {
|
||||
'use cache'
|
||||
cacheTag(`product-${id}`)
|
||||
cacheLife('hours')
|
||||
|
||||
return await db.products.findUnique({ where: { id } })
|
||||
}
|
||||
|
||||
export default async function ProductPage({ params }) {
|
||||
const product = await getProduct(params.id)
|
||||
return <ProductCard product={product} />
|
||||
}
|
||||
|
||||
// ✅ OPTION 2: Wrap in Suspense (streams dynamically)
|
||||
export default async function ProductPage({ params }) {
|
||||
return (
|
||||
<Suspense fallback={<ProductSkeleton />}>
|
||||
<ProductContent id={params.id} />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
> **See also**: Pattern 5 (Cached Data Fetching Functions) in PATTERNS.md shows reusable cached data fetcher patterns.
|
||||
|
||||
---
|
||||
|
||||
## Error: Empty generateStaticParams
|
||||
|
||||
### Symptoms
|
||||
|
||||
```
|
||||
Error: generateStaticParams must return at least one parameter set
|
||||
```
|
||||
|
||||
### Cause
|
||||
|
||||
With Cache Components, empty `generateStaticParams` is no longer allowed. This prevents a class of bugs where dynamic API usage in components would only error in production.
|
||||
|
||||
### Why This Changed
|
||||
|
||||
**Before**: Empty array = "trust me, this is static". Dynamic API usage in production caused runtime errors.
|
||||
|
||||
**After**: Must provide at least one param set so Next.js can validate the page actually renders statically.
|
||||
|
||||
### Solution
|
||||
|
||||
```tsx
|
||||
// ❌ ERROR: Empty array
|
||||
export function generateStaticParams() {
|
||||
return []
|
||||
}
|
||||
|
||||
// ✅ CORRECT: Provide at least one param
|
||||
export async function generateStaticParams() {
|
||||
const products = await getPopularProducts()
|
||||
return products.map(({ category, slug }) => ({ category, slug }))
|
||||
}
|
||||
|
||||
// ✅ ALSO CORRECT: Hardcoded for known routes
|
||||
export function generateStaticParams() {
|
||||
return [{ slug: 'about' }, { slug: 'contact' }, { slug: 'pricing' }]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error: Request Data Inside Cache
|
||||
|
||||
### Symptoms
|
||||
|
||||
```
|
||||
Error: Cannot access cookies/headers inside 'use cache'
|
||||
```
|
||||
|
||||
### Cause
|
||||
|
||||
Cache contexts cannot depend on request-specific data because the cached result would be shared across all users.
|
||||
|
||||
### Solution
|
||||
|
||||
User-specific content should **not be cached**. Remove `'use cache'` and stream the content dynamically:
|
||||
|
||||
```tsx
|
||||
// ❌ ERROR: Cookies inside cache
|
||||
async function UserDashboard() {
|
||||
'use cache'
|
||||
const session = await cookies() // Error!
|
||||
return await fetchDashboard(session.get('userId'))
|
||||
}
|
||||
|
||||
// ✅ CORRECT: Don't cache user-specific content
|
||||
async function UserDashboard() {
|
||||
const session = await cookies()
|
||||
return await fetchDashboard(session.get('userId')?.value)
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Suspense fallback={<DashboardSkeleton />}>
|
||||
<UserDashboard /> {/* Streams at request time */}
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Key insight**: Cache Components are for content that can be shared across users. User-specific dashboards should stream dynamically.
|
||||
|
||||
---
|
||||
|
||||
## Issue: Cache Not Being Used
|
||||
|
||||
### Symptoms
|
||||
|
||||
- Data always fresh on every request
|
||||
- No caching behavior observed
|
||||
- Build logs don't show cached routes
|
||||
|
||||
### Checklist
|
||||
|
||||
**1. Is `cacheComponents` enabled?**
|
||||
|
||||
```typescript
|
||||
// next.config.ts
|
||||
const nextConfig: NextConfig = {
|
||||
cacheComponents: true, // Required!
|
||||
}
|
||||
```
|
||||
|
||||
**2. Is the function async?**
|
||||
|
||||
```tsx
|
||||
// Must be async
|
||||
async function CachedData() {
|
||||
'use cache'
|
||||
return await fetchData()
|
||||
}
|
||||
```
|
||||
|
||||
**3. Is `'use cache'` the first statement?**
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG: Directive not first
|
||||
async function CachedData() {
|
||||
const x = 1 // Something before 'use cache'
|
||||
;('use cache')
|
||||
return await fetchData()
|
||||
}
|
||||
|
||||
// ✅ CORRECT: Directive first
|
||||
async function CachedData() {
|
||||
'use cache'
|
||||
const x = 1
|
||||
return await fetchData()
|
||||
}
|
||||
```
|
||||
|
||||
**4. Are arguments serializable?**
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG: Function as argument (not serializable)
|
||||
async function CachedData({ transform }: { transform: (x: any) => any }) {
|
||||
'use cache'
|
||||
const data = await fetchData()
|
||||
return transform(data)
|
||||
}
|
||||
|
||||
// ✅ CORRECT: Only serializable arguments
|
||||
async function CachedData({ transformType }: { transformType: string }) {
|
||||
'use cache'
|
||||
const data = await fetchData()
|
||||
return applyTransform(data, transformType)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Issue: Stale Data After Mutation
|
||||
|
||||
### Symptoms
|
||||
|
||||
- Created/updated data doesn't appear immediately
|
||||
- Need to refresh page to see changes
|
||||
|
||||
### Cause
|
||||
|
||||
Cache not invalidated after mutation.
|
||||
|
||||
### Solutions
|
||||
|
||||
**1. Use `updateTag()` for immediate consistency:**
|
||||
|
||||
```tsx
|
||||
'use server'
|
||||
import { updateTag } from 'next/cache'
|
||||
|
||||
export async function createPost(data: FormData) {
|
||||
await db.posts.create({ data })
|
||||
updateTag('posts') // Immediate invalidation
|
||||
}
|
||||
```
|
||||
|
||||
**2. Ensure tags match:**
|
||||
|
||||
```tsx
|
||||
// Cache uses this tag
|
||||
async function Posts() {
|
||||
'use cache'
|
||||
cacheTag('posts') // Must match invalidation tag
|
||||
return await db.posts.findMany()
|
||||
}
|
||||
|
||||
// Invalidation must use same tag
|
||||
export async function createPost(data: FormData) {
|
||||
await db.posts.create({ data })
|
||||
updateTag('posts') // Same tag!
|
||||
}
|
||||
```
|
||||
|
||||
**3. Invalidate all relevant tags:**
|
||||
|
||||
```tsx
|
||||
export async function updatePost(postId: string, data: FormData) {
|
||||
const post = await db.posts.update({
|
||||
where: { id: postId },
|
||||
data,
|
||||
})
|
||||
|
||||
// Invalidate all affected caches
|
||||
updateTag('posts') // All posts list
|
||||
updateTag(`post-${postId}`) // Specific post
|
||||
updateTag(`author-${post.authorId}`) // Author's posts
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Issue: Different Cache Values for Same Key
|
||||
|
||||
### Symptoms
|
||||
|
||||
- Cache returns different values for what should be the same query
|
||||
- Inconsistent behavior across requests
|
||||
|
||||
### Cause
|
||||
|
||||
Arguments are part of cache key. Different argument values = different cache entries.
|
||||
|
||||
### Solution
|
||||
|
||||
Normalize arguments:
|
||||
|
||||
```tsx
|
||||
// ❌ Problem: Object reference differs
|
||||
async function CachedData({ options }: { options: { limit: number } }) {
|
||||
'use cache'
|
||||
return await fetchData(options)
|
||||
}
|
||||
|
||||
// Each call creates new object = new cache key
|
||||
<CachedData options={{ limit: 10 }} />
|
||||
<CachedData options={{ limit: 10 }} /> // Different cache entry!
|
||||
|
||||
// ✅ Solution: Use primitives or stable references
|
||||
async function CachedData({ limit }: { limit: number }) {
|
||||
'use cache'
|
||||
return await fetchData({ limit })
|
||||
}
|
||||
|
||||
<CachedData limit={10} />
|
||||
<CachedData limit={10} /> // Same cache entry!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Issue: Cache Too Aggressive (Stale Data)
|
||||
|
||||
### Symptoms
|
||||
|
||||
- Data doesn't update when expected
|
||||
- Users see outdated content
|
||||
|
||||
### Solutions
|
||||
|
||||
**1. Reduce cache lifetime:**
|
||||
|
||||
```tsx
|
||||
async function FrequentlyUpdatedData() {
|
||||
'use cache'
|
||||
cacheLife('seconds') // Short cache
|
||||
|
||||
// Or custom short duration
|
||||
cacheLife({
|
||||
stale: 0,
|
||||
revalidate: 30,
|
||||
expire: 60,
|
||||
})
|
||||
|
||||
return await fetchData()
|
||||
}
|
||||
```
|
||||
|
||||
**2. Don't cache volatile data:**
|
||||
|
||||
```tsx
|
||||
// For truly real-time data, skip caching
|
||||
async function LiveData() {
|
||||
// No 'use cache'
|
||||
return await fetchLiveData()
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<LiveData />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Issue: Build Takes Too Long
|
||||
|
||||
### Symptoms
|
||||
|
||||
- Build hangs during prerendering
|
||||
- Timeout errors during `next build`
|
||||
|
||||
### Cause
|
||||
|
||||
Cached functions making slow network requests or accessing unavailable services during build.
|
||||
|
||||
### Solutions
|
||||
|
||||
**1. Use fallback data for build:**
|
||||
|
||||
```tsx
|
||||
async function CachedData() {
|
||||
'use cache'
|
||||
|
||||
try {
|
||||
return await fetchFromAPI()
|
||||
} catch (error) {
|
||||
// Return fallback during build if API unavailable
|
||||
return getFallbackData()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**2. Limit static generation scope:**
|
||||
|
||||
```tsx
|
||||
// app/[slug]/page.tsx
|
||||
export function generateStaticParams() {
|
||||
// Only prerender most important pages at build time
|
||||
// Other pages will be generated on-demand at request time
|
||||
return [{ slug: 'home' }, { slug: 'about' }]
|
||||
}
|
||||
```
|
||||
|
||||
**3. Use Suspense for truly dynamic content:**
|
||||
|
||||
```tsx
|
||||
// app/[slug]/page.tsx
|
||||
import { Suspense } from 'react'
|
||||
|
||||
export default function Page({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>
|
||||
}) {
|
||||
return (
|
||||
<Suspense fallback={<PageSkeleton />}>
|
||||
<DynamicContent params={params} />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** Avoid using `export const dynamic = 'force-dynamic'` as this segment config is deprecated with Cache Components. Use Suspense boundaries and `'use cache'` for granular control instead.
|
||||
|
||||
---
|
||||
|
||||
## Debugging Techniques
|
||||
|
||||
### 1. Check Cache Headers
|
||||
|
||||
In development, inspect response headers:
|
||||
|
||||
```bash
|
||||
curl -I http://localhost:3000/your-page
|
||||
```
|
||||
|
||||
Look for:
|
||||
|
||||
- `x-nextjs-cache: HIT` - Served from cache
|
||||
- `x-nextjs-cache: MISS` - Cache miss, recomputed
|
||||
- `x-nextjs-cache: STALE` - Stale content, revalidating
|
||||
|
||||
### 2. Enable Verbose Logging
|
||||
|
||||
```bash
|
||||
# Environment variable for cache debugging
|
||||
NEXT_PRIVATE_DEBUG_CACHE=1 npm run dev
|
||||
```
|
||||
|
||||
### 3. Check Build Output
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
|
||||
# Look for:
|
||||
# ○ (Static) - Fully static
|
||||
# ◐ (Partial) - Partial prerender with cache
|
||||
# λ (Dynamic) - Server-rendered
|
||||
```
|
||||
|
||||
### 4. Inspect Cache Tags
|
||||
|
||||
Add logging to verify tags:
|
||||
|
||||
```tsx
|
||||
async function CachedData({ id }: { id: string }) {
|
||||
'use cache'
|
||||
|
||||
const tags = ['data', `item-${id}`]
|
||||
console.log('Cache tags:', tags) // Check during build
|
||||
|
||||
tags.forEach((tag) => cacheTag(tag))
|
||||
cacheLife('hours')
|
||||
|
||||
return await fetchData(id)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes Checklist
|
||||
|
||||
| Mistake | Symptom | Fix |
|
||||
| ---------------------------------- | ------------------ | --------------------- |
|
||||
| Missing `cacheComponents: true` | No caching | Add to next.config.ts |
|
||||
| Sync function with `'use cache'` | Build error | Make function async |
|
||||
| `'use cache'` not first statement | Cache ignored | Move to first line |
|
||||
| Accessing cookies/headers in cache | Timeout error | Extract to wrapper |
|
||||
| Non-serializable arguments | Inconsistent cache | Use primitives |
|
||||
| Missing Suspense for dynamic | Streaming broken | Wrap in Suspense |
|
||||
| Wrong tag in invalidation | Stale data | Match cache tags |
|
||||
| Over-caching volatile data | Stale data | Reduce cacheLife |
|
||||
|
||||
---
|
||||
|
||||
## Performance Optimization Tips
|
||||
|
||||
### 1. Profile Cache Hit Rates
|
||||
|
||||
Monitor cache effectiveness:
|
||||
|
||||
```tsx
|
||||
async function CachedData() {
|
||||
'use cache'
|
||||
|
||||
const start = performance.now()
|
||||
const data = await fetchData()
|
||||
const duration = performance.now() - start
|
||||
|
||||
// Log for analysis
|
||||
console.log(`Cache execution: ${duration}ms`)
|
||||
|
||||
return data
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Optimize Cache Granularity
|
||||
|
||||
```tsx
|
||||
// ❌ Coarse: One big cached component
|
||||
async function PageContent() {
|
||||
'use cache'
|
||||
const header = await fetchHeader()
|
||||
const posts = await fetchPosts()
|
||||
const sidebar = await fetchSidebar()
|
||||
return <>{/* everything */}</>
|
||||
}
|
||||
|
||||
// ✅ Fine-grained: Independent cached components
|
||||
async function Header() {
|
||||
'use cache'
|
||||
cacheLife('days')
|
||||
return await fetchHeader()
|
||||
}
|
||||
|
||||
async function Posts() {
|
||||
'use cache'
|
||||
cacheLife('hours')
|
||||
return await fetchPosts()
|
||||
}
|
||||
|
||||
async function Sidebar() {
|
||||
'use cache'
|
||||
cacheLife('minutes')
|
||||
return await fetchSidebar()
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Strategic Tag Design
|
||||
|
||||
```tsx
|
||||
// Hierarchical tags for targeted invalidation
|
||||
cacheTag(
|
||||
'posts', // All posts
|
||||
`category-${category}`, // Posts in category
|
||||
`post-${id}`, // Specific post
|
||||
`author-${authorId}` // Author's posts
|
||||
)
|
||||
|
||||
// Invalidate at appropriate level
|
||||
updateTag(`post-${id}`) // Single post changed
|
||||
updateTag(`author-${author}`) // Author updated all posts
|
||||
updateTag('posts') // Nuclear option
|
||||
```
|
||||
Reference in New Issue
Block a user