cache-components

Next.js Cache Components Skill

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "cache-components" with this command: npx skills add sgcarstrends/sgcarstrends/sgcarstrends-sgcarstrends-cache-components

Next.js Cache Components Skill

This skill helps you implement and optimize Next.js 16 Cache Components in apps/web/ .

When to Use This Skill

  • Converting components to use Cache Components

  • Adding "use cache" directives to functions or queries

  • Implementing query-level caching for data fetching

  • Setting up domain-level cache tags for efficient invalidation

  • Implementing Suspense boundaries

  • Configuring cacheLife profiles

  • Debugging cache-related errors

  • Optimizing page load performance

  • Reducing ISR write operations with broad cache tags

Cache Components Overview

Next.js 16 introduced Cache Components to improve performance by caching component render results.

Key Concepts

  • "use cache" directive: Marks functions/components for caching

  • Query-level caching: Apply cache directives at data-fetching query functions for consistency

  • Domain-level cache tags: Use broad tags (e.g., CACHE_LIFE.cars ) instead of granular per-record tags

  • Suspense boundaries: Required for streaming cached content

  • cacheLife profiles: Control cache duration and invalidation

  • cacheTag: Manual cache invalidation (prefer domain-level scopes)

  • Dynamic vs Static: Understanding when caching applies

Implementation Patterns

  1. Basic Cache Component

// app/components/car-list.tsx import { Suspense } from "react";

async function CarList() { "use cache";

const cars = await db.query.cars.findMany();

return ( <div> {cars.map(car => ( <div key={car.id}>{car.make} {car.model}</div> ))} </div> ); }

// Parent component with Suspense export default function CarsPage() { return ( <Suspense fallback={<div>Loading cars...</div>}> <CarList /> </Suspense> ); }

  1. Cache with cacheLife

Control cache duration:

import { cacheLife } from "next/cache";

async function COEData() { "use cache"; cacheLife("hours"); // Built-in profile: cache for hours

const coe = await db.query.coe.findMany(); return <COETable data={coe} />; }

// Custom cache profile async function RealtimeData() { "use cache"; cacheLife({ stale: 60, // Serve stale for 60 seconds revalidate: 300, // Revalidate after 5 minutes expire: 3600, // Expire after 1 hour });

const data = await fetchRealtimeData(); return <DataDisplay data={data} />; }

  1. Cache Tags for Invalidation

Domain-Level Scopes (Recommended)

Use broad, domain-level cache tags to minimize ISR write operations:

// lib/cache.ts - Define domain-level cache scopes export const CACHE_LIFE = { cars: "cars", coe: "coe", posts: "posts", } as const;

// Query function with domain-level cache tag import { CACHE_LIFE } from "@web/lib/cache"; import { cacheLife, cacheTag } from "next/cache";

export const getDistinctMakes = async () => { "use cache"; cacheLife("max"); cacheTag(CACHE_LIFE.cars); // Broad tag for all car-related data

return db.selectDistinct({ make: cars.make }).from(cars).orderBy(cars.make); };

// Invalidate entire domain cache in server action "use server"; import { revalidateTag } from "next/cache"; import { CACHE_LIFE } from "@web/lib/cache";

export async function updateCarData() { await fetchAndUpdateCars(); revalidateTag(CACHE_LIFE.cars); // Invalidate all car-related caches }

Granular Tags (Use Sparingly)

For specific invalidation needs, use granular tags:

async function BlogPosts() { "use cache"; cacheTag("blog-posts"); // Tag for manual invalidation

const posts = await db.query.posts.findMany(); return <PostList posts={posts} />; }

// Invalidate cache in server action "use server"; import { revalidateTag } from "next/cache";

export async function createPost(data: PostData) { await db.insert(posts).values(data); revalidateTag("blog-posts"); // Invalidate cached blog posts }

⚠️ Cache Tag Best Practices:

  • Prefer domain-level tags to reduce ISR write operations

  • Avoid over-granular tags (e.g., per-record tags) which increase overhead

  • Use granular tags only when selective invalidation is critical

  • Keep tag names consistent using constants from lib/cache.ts

  1. Query-Level Caching (Recommended Pattern)

Apply cache directives at the query function level for reusable data fetching:

// queries/cars/filter-options.ts import { cars, db } from "@sgcarstrends/database"; import { CACHE_LIFE } from "@web/lib/cache"; import { cacheLife, cacheTag } from "next/cache";

export const getDistinctMakes = async () => { "use cache"; cacheLife("max"); cacheTag(CACHE_LIFE.cars);

return db.selectDistinct({ make: cars.make }).from(cars).orderBy(cars.make); };

export const getDistinctFuelTypes = async (month?: string) => { "use cache"; cacheLife("max"); cacheTag(CACHE_LIFE.cars);

const filters = month ? [eq(cars.month, month)] : []; return db .selectDistinct({ fuelType: cars.fuelType }) .from(cars) .where(filters.length > 0 ? and(...filters) : undefined) .orderBy(cars.fuelType); };

Benefits:

  • ✅ Centralized cache configuration for all data queries

  • ✅ Consistent caching strategy across the application

  • ✅ Easy to maintain and update cache policies

  • ✅ Reusable across multiple components/pages

  • ✅ Domain-level cache tags for efficient invalidation

Usage in Components:

// app/cars/page.tsx import { getDistinctMakes } from "@web/queries/cars/filter-options";

export default async function CarsPage() { const makes = await getDistinctMakes(); // Cached automatically

return <MakesList makes={makes} />; }

  1. Private Cache (User-Specific)

import { cookies } from "next/headers";

async function UserDashboard() { "use cache: private"; // Cache per-user, not globally

const cookieStore = await cookies(); const userId = cookieStore.get("userId")?.value; const userData = await fetchUserData(userId);

return <Dashboard data={userData} />; }

  1. Nested Cache Components

// Outer component - longer cache async function CarsByMake({ make }: { make: string }) { "use cache"; cacheLife("days");

const cars = await db.query.cars.findMany({ where: eq(cars.make, make), });

return ( <div> <h2>{make} Models</h2> <Suspense fallback={<div>Loading stats...</div>}> <CarStats makeId={make} /> </Suspense> {cars.map(car => <CarCard key={car.id} car={car} />)} </div> ); }

// Inner component - shorter cache async function CarStats({ makeId }: { makeId: string }) { "use cache"; cacheLife("minutes");

const stats = await calculateStats(makeId); return <StatsDisplay stats={stats} />; }

Common Tasks

Converting Existing Component to Cache Component

Before:

// Regular server component async function DataTable() { const data = await fetchData(); return <Table data={data} />; }

After:

// Cached server component async function DataTable() { "use cache"; cacheLife("hours"); cacheTag("data-table");

const data = await fetchData(); return <Table data={data} />; }

// Parent with Suspense export default function Page() { return ( <Suspense fallback={<TableSkeleton />}> <DataTable /> </Suspense> ); }

Debugging Cache Issues

Component not caching:

  • Verify "use cache" is first line

  • Check Suspense boundary exists

  • Ensure running Next.js 16+

  • Check dev server logs

Cache not invalidating:

  • Verify cacheTag is set correctly

  • Check revalidateTag is called

  • Review cacheLife settings

  • Check if using "private" cache incorrectly

Runtime errors:

  • Use Next.js runtime MCP tool to check errors

  • Review browser console for hydration issues

  • Check server logs for cache misses

Use Next.js DevTools MCP:

// Get runtime errors mcp__next-devtools__nextjs_runtime({ action: "call_tool", toolName: "get_errors" })

Performance Optimization

Identify cache opportunities:

  • Expensive data fetches: Wrap in "use cache"

  • Static content: Use long cacheLife

  • Frequently accessed: Cache with appropriate TTL

  • User-specific: Use "use cache: private"

Built-in cacheLife Profiles

Next.js provides these profiles:

"seconds" // { stale: 1, revalidate: 10, expire: 60 } "minutes" // { stale: 60, revalidate: 300, expire: 3600 } "hours" // { stale: 3600, revalidate: 86400, expire: 604800 } "days" // { stale: 86400, revalidate: 604800, expire: 2592000 } "weeks" // { stale: 604800, revalidate: 2592000, expire: 31536000 } "max" // { stale: 2592000, revalidate: 31536000, expire: Infinity }

Suspense Best Practices

  1. Loading States

Provide meaningful fallbacks:

<Suspense fallback={<CarListSkeleton />}> <CarList /> </Suspense>

  1. Multiple Suspense Boundaries

Stream different parts independently:

export default function Dashboard() { return ( <> <Suspense fallback={<HeaderSkeleton />}> <Header /> </Suspense>

  &#x3C;Suspense fallback={&#x3C;ChartSkeleton />}>
    &#x3C;Charts />
  &#x3C;/Suspense>

  &#x3C;Suspense fallback={&#x3C;TableSkeleton />}>
    &#x3C;DataTable />
  &#x3C;/Suspense>
&#x3C;/>

); }

  1. Error Boundaries with Suspense

import { ErrorBoundary } from "react-error-boundary";

export default function Page() { return ( <ErrorBoundary fallback={<ErrorDisplay />}> <Suspense fallback={<Loading />}> <DataComponent /> </Suspense> </ErrorBoundary> ); }

Testing Cache Components

// tests/car-list.test.tsx import { render, screen } from "@testing-library/react"; import { Suspense } from "react"; import CarList from "../car-list";

describe("CarList", () => { it("renders with Suspense", async () => { render( <Suspense fallback={<div>Loading...</div>}> <CarList /> </Suspense> );

// Initially shows fallback
expect(screen.getByText("Loading...")).toBeInTheDocument();

// Wait for component to load
const content = await screen.findByText(/Toyota/i);
expect(content).toBeInTheDocument();

}); });

Common Errors and Solutions

Error: "use cache" must be first directive

Wrong:

async function Component() { const data = await fetchData(); "use cache"; // ❌ Too late }

Correct:

async function Component() { "use cache"; // ✅ First line const data = await fetchData(); }

Error: Missing Suspense boundary

Wrong:

export default function Page() { return <CachedComponent />; // ❌ No Suspense }

Correct:

export default function Page() { return ( <Suspense fallback={<Loading />}> <CachedComponent /> </Suspense> ); }

Error: Dynamic rendering disables cache

Avoid dynamic APIs in cached components:

Wrong:

async function Component() { "use cache"; const headers = await headers(); // ❌ Dynamic API }

Correct:

// Move dynamic logic to parent export default async function Page() { const headers = await headers(); const userId = headers.get("x-user-id");

return ( <Suspense fallback={<Loading />}> <CachedComponent userId={userId} /> </Suspense> ); }

async function CachedComponent({ userId }: { userId: string }) { "use cache"; // Now receives userId as prop }

Documentation References

Always check the latest Next.js docs:

// Query Next.js documentation mcp__next-devtools__nextjs_docs({ action: "get", path: "/docs/app/api-reference/directives/use-cache" })

Performance Monitoring

Track cache effectiveness:

  • Check Next.js build output for cached routes

  • Monitor server response times

  • Use Next.js Analytics for performance metrics

  • Review cache hit/miss ratios in logs

References

  • Next.js 16 Cache Components: Use nextjs_docs MCP tool

  • Related files:

  • apps/web/src/app/

  • All Next.js pages

  • apps/web/src/components/

  • React components

  • apps/web/CLAUDE.md

  • Web app documentation

Best Practices

  • Query-Level Caching: Apply cache directives at query functions for consistency and reusability

  • Domain-Level Tags: Use broad cache tags (e.g., CACHE_LIFE.cars ) to minimize ISR write operations

  • Avoid Over-Granular Tags: Don't create per-record or per-field tags; use domain scopes instead

  • Appropriate TTLs: Match cache duration to data freshness needs (use cacheLife("max") for static data)

  • Cache Tag Constants: Define cache tags in lib/cache.ts for consistency

  • Error Handling: Always wrap cached components with ErrorBoundary

  • Loading States: Provide meaningful Suspense fallbacks

  • Testing: Test both cached and uncached states

  • Monitoring: Track cache performance in production

  • Progressive Enhancement: Start with long TTLs, optimize based on usage

Cache Optimization Strategy

Recommended Approach (SG Cars Trends Pattern):

// 1. Define domain-level cache tags // lib/cache.ts export const CACHE_LIFE = { cars: "cars", coe: "coe", posts: "posts", } as const;

// 2. Apply cache directives at query level // queries/cars/filter-options.ts export const getDistinctMakes = async () => { "use cache"; cacheLife("max"); cacheTag(CACHE_LIFE.cars);

return db.selectDistinct({ make: cars.make }).from(cars); };

// 3. Invalidate at domain level // actions/update-cars.ts "use server"; import { revalidateTag } from "next/cache"; import { CACHE_LIFE } from "@web/lib/cache";

export async function updateCarData() { await fetchAndUpdateCars(); revalidateTag(CACHE_LIFE.cars); // Invalidate all car-related caches }

Benefits:

  • ✅ Reduces ISR write operations

  • ✅ Simplifies cache invalidation

  • ✅ Improves performance

  • ✅ Easier to maintain

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

framer-motion-animations

No summary provided by upstream source.

Repository SourceNeeds Review
General

shadcn-components

No summary provided by upstream source.

Repository SourceNeeds Review
General

api-testing

No summary provided by upstream source.

Repository SourceNeeds Review
General

design-language-system

No summary provided by upstream source.

Repository SourceNeeds Review