Back to skills
SkillHub ClubShip Full StackFull StackFrontend

cache-components

This skill implements Next.js 16 Cache Components to optimize page performance by adding 'use cache' directives, configuring cacheLife profiles, setting up Suspense boundaries, and using domain-level cache tags for efficient invalidation.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
20
Hot score
87
Updated
March 20, 2026
Overall rating
C4.4
Composite score
4.4
Best-practice grade
B78.7

Install command

npx @skill-hub/cli install sgcarstrends-backend-cache-components

Repository

sgcarstrends/backend

Skill path: .claude/skills/cache-components

This skill implements Next.js 16 Cache Components to optimize page performance by adding 'use cache' directives, configuring cacheLife profiles, setting up Suspense boundaries, and using domain-level cache tags for efficient invalidation.

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack, Frontend.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: sgcarstrends.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install cache-components into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/sgcarstrends/backend before adding cache-components to shared team environments
  • Use cache-components for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: cache-components
description: Implement Next.js 16 Cache Components with use cache directives, Suspense boundaries, and cacheLife profiles. Use when optimizing page performance or migrating components to cache mode.
allowed-tools: Read, Edit, mcp__next-devtools__nextjs_docs, mcp__next-devtools__nextjs_runtime
---

# 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

1. **"use cache" directive**: Marks functions/components for caching
2. **Query-level caching**: Apply cache directives at data-fetching query functions for consistency
3. **Domain-level cache tags**: Use broad tags (e.g., `CACHE_LIFE.cars`) instead of granular per-record tags
4. **Suspense boundaries**: Required for streaming cached content
5. **cacheLife profiles**: Control cache duration and invalidation
6. **cacheTag**: Manual cache invalidation (prefer domain-level scopes)
7. **Dynamic vs Static**: Understanding when caching applies

## Implementation Patterns

### 1. Basic Cache Component

```typescript
// 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>
  );
}
```

### 2. Cache with cacheLife

Control cache duration:

```typescript
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} />;
}
```

### 3. Cache Tags for Invalidation

**Domain-Level Scopes (Recommended)**

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

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

```typescript
// 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);
};
```

```typescript
// 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:

```typescript
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`

### 4. Query-Level Caching (Recommended Pattern)

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

```typescript
// 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:**

```typescript
// 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} />;
}
```

### 5. Private Cache (User-Specific)

```typescript
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} />;
}
```

### 6. Nested Cache Components

```typescript
// 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:**
```typescript
// Regular server component
async function DataTable() {
  const data = await fetchData();
  return <Table data={data} />;
}
```

**After:**
```typescript
// 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

1. **Component not caching:**
   - Verify "use cache" is first line
   - Check Suspense boundary exists
   - Ensure running Next.js 16+
   - Check dev server logs

2. **Cache not invalidating:**
   - Verify cacheTag is set correctly
   - Check revalidateTag is called
   - Review cacheLife settings
   - Check if using "private" cache incorrectly

3. **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:
```typescript
// Get runtime errors
mcp__next-devtools__nextjs_runtime({ action: "call_tool", toolName: "get_errors" })
```

### Performance Optimization

Identify cache opportunities:

1. **Expensive data fetches**: Wrap in "use cache"
2. **Static content**: Use long cacheLife
3. **Frequently accessed**: Cache with appropriate TTL
4. **User-specific**: Use "use cache: private"

## Built-in cacheLife Profiles

Next.js provides these profiles:

```typescript
"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:

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

### 2. Multiple Suspense Boundaries

Stream different parts independently:

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

      <Suspense fallback={<ChartSkeleton />}>
        <Charts />
      </Suspense>

      <Suspense fallback={<TableSkeleton />}>
        <DataTable />
      </Suspense>
    </>
  );
}
```

### 3. Error Boundaries with Suspense

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

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

## Testing Cache Components

```typescript
// __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:**
```typescript
async function Component() {
  const data = await fetchData();
  "use cache"; // ❌ Too late
}
```

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

### Error: Missing Suspense boundary

**Wrong:**
```typescript
export default function Page() {
  return <CachedComponent />; // ❌ No Suspense
}
```

**Correct:**
```typescript
export default function Page() {
  return (
    <Suspense fallback={<Loading />}>
      <CachedComponent />
    </Suspense>
  );
}
```

### Error: Dynamic rendering disables cache

Avoid dynamic APIs in cached components:

**Wrong:**
```typescript
async function Component() {
  "use cache";
  const headers = await headers(); // ❌ Dynamic API
}
```

**Correct:**
```typescript
// 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:

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

## Performance Monitoring

Track cache effectiveness:

1. Check Next.js build output for cached routes
2. Monitor server response times
3. Use Next.js Analytics for performance metrics
4. 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

1. **Query-Level Caching**: Apply cache directives at query functions for consistency and reusability
2. **Domain-Level Tags**: Use broad cache tags (e.g., `CACHE_LIFE.cars`) to minimize ISR write operations
3. **Avoid Over-Granular Tags**: Don't create per-record or per-field tags; use domain scopes instead
4. **Appropriate TTLs**: Match cache duration to data freshness needs (use `cacheLife("max")` for static data)
5. **Cache Tag Constants**: Define cache tags in `lib/cache.ts` for consistency
6. **Error Handling**: Always wrap cached components with ErrorBoundary
7. **Loading States**: Provide meaningful Suspense fallbacks
8. **Testing**: Test both cached and uncached states
9. **Monitoring**: Track cache performance in production
10. **Progressive Enhancement**: Start with long TTLs, optimize based on usage

### Cache Optimization Strategy

**Recommended Approach (SG Cars Trends Pattern):**

```typescript
// 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
cache-components | SkillHub