data-fetching
About
This skill provides Next.js 16+ best practices for server-side data fetching, caching, and rendering using Cache Components and Partial Prerendering. It teaches developers to implement fine-grained caching at the component level with `use cache` directives and lifetime profiles. Use it to optimize performance by separating static and dynamic content within Suspense boundaries while enforcing structured data access through server actions.
Quick Install
Claude Code
Recommended/plugin add https://github.com/majiayu000/claude-skill-registrygit clone https://github.com/majiayu000/claude-skill-registry.git ~/.claude/skills/data-fetchingCopy and paste this command in Claude Code to install this skill
Documentation
Overview
This skill covers server-side data fetching and caching patterns using Next.js 16+ Cache Components approach with Partial Prerendering (PPR). It combines fine-grained caching control with server-side data fetching for optimal performance.
Core Principles
Data Access Rules
- NEVER call Drizzle ORM directly - Always use server actions defined in
lib/actions/ - Cache at the component level - Use the
use cachedirective in pages/layouts, not in action files - Wrap dynamic content - Use
Suspenseboundaries to separate static and dynamic content - Use lifetime profiles - Always specify
cacheLife()with appropriate profile
Cache Components Workflow
1. Planning Data Fetching
Before implementing:
- Identify what data is needed for the page/component
- Determine what content should be instantly visible (cached) vs. what can stream (dynamic)
- Locate the appropriate server actions in
lib/actions/or create new ones if needed - Plan cache tags for data that needs manual invalidation
2. Implementing Cached Data Fetching
Follow this pattern in pages or layouts:
import { cacheLife } from 'next/cache'
import { getModels } from '@/lib/actions/models'
export default async function ModelsPage() {
'use cache'
cacheLife('hours')
const models = await getModels()
return <div>{/* render models */}</div>
}
3. Handling Dynamic Content
For runtime-dependent data (cookies, headers, searchParams):
import { Suspense } from 'react'
export default function Page() {
return (
<>
<h1>Static Content</h1>
<Suspense fallback={<Skeleton />}>
<DynamicUserContent />
</Suspense>
</>
)
}
async function DynamicUserContent() {
const session = await getSession() // uses cookies()
return <div>{session.user.name}</div>
}
Caching Configuration
Cache Life Profiles
Use built-in lifetime profiles with cacheLife():
| Profile | Use Case | Duration |
|---|---|---|
'seconds' | Highly volatile data | ~30 seconds |
'minutes' | Frequently updated content | ~5 minutes |
'hours' | Semi-static content | ~1 hour |
'days' | Mostly static content | ~1 day |
'weeks' | Rarely changing content | ~1 week |
'max' | Static content | Maximum duration |
Default Choice: Use 'hours' for most content unless you have specific requirements.
Cache Tags and Revalidation
Using cacheTag for Manual Invalidation
Tag cached data that needs to be invalidated on specific events:
import { cacheLife, cacheTag } from 'next/cache'
import { getModelById } from '@/lib/actions/models'
export default async function ModelPage({ params }: { params: { id: string } }) {
'use cache'
cacheLife('hours')
cacheTag('models', `model-${params.id}`)
const model = await getModelById(params.id)
return <div>{/* render model */}</div>
}
Invalidating Cache with updateTag
Use in server actions for immediate cache expiration (read-your-own-writes):
'use server'
import { updateTag } from 'next/cache'
export async function updateModel(id: string, data: ModelData) {
// Update database via action
await updateModelAction(id, data)
// Immediately expire cache so user sees fresh data
updateTag(`model-${id}`, 'models')
}
Using revalidateTag for Background Refresh
For stale-while-revalidate pattern:
'use server'
import { revalidateTag } from 'next/cache'
export async function createModel(data: ModelData) {
await createModelAction(data)
// Stale-while-revalidate: serve stale, refresh in background
revalidateTag('models', 'max')
}
Best Practices
Caching Strategy
- Cache pages/layouts, not actions - Add
use cachedirective in pages/layouts that consume actions, never in action files themselves - Wrap actions in cached functions - The page/layout function itself becomes the caching boundary
- Use Suspense boundaries - Separate static shell from dynamic/streaming content
- Tag strategically - Use cache tags for content that changes infrequently but needs manual updates
Performance Optimization
- Minimize dynamic APIs - Avoid using
cookies(),headers(), orsearchParamsin cached functions - Parallel data fetching - Multiple server actions can be called in parallel within a cached component
- Appropriate cache lifetimes - Balance freshness needs with server load
Data Mutation Patterns
- Use
updateTagfor user mutations - When users need to see their changes immediately - Use
revalidateTagfor background updates - When serving slightly stale data is acceptable - Tag hierarchies - Use multiple tags (e.g.,
'models'and'model-123') for flexible invalidation
Common Patterns
Pattern 1: Cached List Page
import { cacheLife, cacheTag } from 'next/cache'
import { getModels } from '@/lib/actions/models'
export default async function ModelsPage() {
'use cache'
cacheLife('hours')
cacheTag('models')
const models = await getModels()
return <div>{/* render list */}</div>
}
Pattern 2: Cached Detail Page with Params
import { cacheLife, cacheTag } from 'next/cache'
import { getModelById } from '@/lib/actions/models'
export default async function ModelPage({ params }: { params: { id: string } }) {
'use cache'
cacheLife('hours')
cacheTag('models', `model-${params.id}`)
const model = await getModelById(params.id)
return <div>{/* render detail */}</div>
}
Pattern 3: Mixed Static and Dynamic Content
import { Suspense } from 'react'
import { cacheLife } from 'next/cache'
export default function Page() {
return (
<>
<StaticContent />
<Suspense fallback={<LoadingSkeleton />}>
<DynamicContent />
</Suspense>
</>
)
}
async function StaticContent() {
'use cache'
cacheLife('hours')
const data = await getStaticData()
return <div>{/* render */}</div>
}
async function DynamicContent() {
const session = await getSession() // uses cookies
const userData = await getUserData(session.userId)
return <div>{/* render */}</div>
}
Pattern 4: Server Action with Cache Invalidation
'use server'
import { updateTag } from 'next/cache'
import { updateModelAction } from '@/lib/actions/models'
export async function updateModel(id: string, data: FormData) {
const result = await updateModelAction(id, data)
if (result.status === 'success') {
// Immediately expire cache for this specific model and all models
updateTag(`model-${id}`, 'models')
}
return result
}
Important Constraints
Serialization Requirements
- Arguments must be serializable - Pass primitives, plain objects, and arrays only
- No class instances or functions - Cannot pass non-serializable values as arguments to cached functions
- Unserializable return values are OK - Can return React components or other unserializable values if you don't introspect them
What NOT to Cache
- Functions using runtime APIs -
cookies(),headers(),searchParamsshould not be in cached functions - Server Actions - Never add
use cacheto server action files; cache at the consumption point - Highly personalized content - User-specific data that varies per request
Configuration
Enable Cache Components in next.config.ts:
const nextConfig = {
cacheComponents: true,
}
export default nextConfig
Troubleshooting
Cache Not Working
- Verify
cacheComponents: trueis set innext.config.ts - Check that
use cacheis at the top of the function body - Ensure you're using Node.js runtime (Edge Runtime not supported)
- Verify function arguments are serializable
Stale Data Issues
- Check cache lifetime profile - may need shorter duration
- Use
updateTaginstead ofrevalidateTagfor immediate updates - Verify cache tags match between caching and invalidation
Performance Issues
- Profile which content needs to be cached vs. dynamic
- Use more
Suspenseboundaries to improve streaming - Consider longer cache lifetimes for stable content
- Review database query performance in server actions
GitHub Repository
Related Skills
content-collections
MetaThis skill provides a production-tested setup for Content Collections, a TypeScript-first tool that transforms Markdown/MDX files into type-safe data collections with Zod validation. Use it when building blogs, documentation sites, or content-heavy Vite + React applications to ensure type safety and automatic content validation. It covers everything from Vite plugin configuration and MDX compilation to deployment optimization and schema validation.
llamaindex
MetaLlamaIndex is a data framework for building RAG-powered LLM applications, specializing in document ingestion, indexing, and querying. It provides key features like vector indices, query engines, and agents, and supports over 300 data connectors. Use it for document Q&A, chatbots, and knowledge retrieval when building data-centric applications.
hybrid-cloud-networking
MetaThis skill configures secure hybrid cloud networking between on-premises infrastructure and cloud platforms like AWS, Azure, and GCP. Use it when connecting data centers to the cloud, building hybrid architectures, or implementing secure cross-premises connectivity. It supports key capabilities such as VPNs and dedicated connections like AWS Direct Connect for high-performance, reliable setups.
polymarket
MetaThis skill enables developers to build applications with the Polymarket prediction markets platform, including API integration for trading and market data. It also provides real-time data streaming via WebSocket to monitor live trades and market activity. Use it for implementing trading strategies or creating tools that process live market updates.
