--- name: data-fetching description: Best practices and conventions for server-side data fetching, caching, and rendering in Next.js 16+ applications. --- ## 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 cache` directive in pages/layouts, not in action files - **Wrap dynamic content** - Use `Suspense` boundaries 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: ```typescript 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
{/* render models */}
} ``` ### 3. Handling Dynamic Content For runtime-dependent data (cookies, headers, searchParams): ```typescript import { Suspense } from 'react' export default function Page() { return ( <>

Static Content

}> ) } async function DynamicUserContent() { const session = await getSession() // uses cookies() return
{session.user.name}
} ``` ## 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: ```typescript 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
{/* render model */}
} ``` #### Invalidating Cache with `updateTag` Use in server actions for immediate cache expiration (read-your-own-writes): ```typescript '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: ```typescript '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 cache` directive 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()`, or `searchParams` in 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 `updateTag` for user mutations** - When users need to see their changes immediately - **Use `revalidateTag` for 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 ```typescript 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
{/* render list */}
} ``` ### Pattern 2: Cached Detail Page with Params ```typescript 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
{/* render detail */}
} ``` ### Pattern 3: Mixed Static and Dynamic Content ```typescript import { Suspense } from 'react' import { cacheLife } from 'next/cache' export default function Page() { return ( <> }> ) } async function StaticContent() { 'use cache' cacheLife('hours') const data = await getStaticData() return
{/* render */}
} async function DynamicContent() { const session = await getSession() // uses cookies const userData = await getUserData(session.userId) return
{/* render */}
} ``` ### Pattern 4: Server Action with Cache Invalidation ```typescript '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()`, `searchParams` should not be in cached functions - **Server Actions** - Never add `use cache` to 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`: ```typescript const nextConfig = { cacheComponents: true, } export default nextConfig ``` ## Troubleshooting ### Cache Not Working - Verify `cacheComponents: true` is set in `next.config.ts` - Check that `use cache` is 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 `updateTag` instead of `revalidateTag` for immediate updates - Verify cache tags match between caching and invalidation ### Performance Issues - Profile which content needs to be cached vs. dynamic - Use more `Suspense` boundaries to improve streaming - Consider longer cache lifetimes for stable content - Review database query performance in server actions