Claude-skill-registry data-fetching
Best practices and conventions for server-side data fetching, caching, and rendering in Next.js 16+ applications.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/data-fetching" ~/.claude/skills/majiayu000-claude-skill-registry-data-fetching && rm -rf "$T"
manifest:
skills/data/data-fetching/SKILL.mdsource content
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
directive in pages/layouts, not in action filesuse cache - Wrap dynamic content - Use
boundaries to separate static and dynamic contentSuspense - Use lifetime profiles - Always specify
with appropriate profilecacheLife()
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
or create new ones if neededlib/actions/ - 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 |
|---|---|---|
| Highly volatile data | ~30 seconds |
| Frequently updated content | ~5 minutes |
| Semi-static content | ~1 hour |
| Mostly static content | ~1 day |
| Rarely changing content | ~1 week |
| 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
cacheTagTag 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
updateTagUse 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
revalidateTagFor 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
directive in pages/layouts that consume actions, never in action files themselvesuse cache - 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()
, orheaders()
in cached functionssearchParams - 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
for user mutations - When users need to see their changes immediatelyupdateTag - Use
for background updates - When serving slightly stale data is acceptablerevalidateTag - Tag hierarchies - Use multiple tags (e.g.,
and'models'
) for flexible invalidation'model-123'
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()
should not be in cached functionssearchParams - Server Actions - Never add
to server action files; cache at the consumption pointuse cache - 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
is set incacheComponents: truenext.config.ts - Check that
is at the top of the function bodyuse cache - 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
instead ofupdateTag
for immediate updatesrevalidateTag - Verify cache tags match between caching and invalidation
Performance Issues
- Profile which content needs to be cached vs. dynamic
- Use more
boundaries to improve streamingSuspense - Consider longer cache lifetimes for stable content
- Review database query performance in server actions