| name | nextjs-cache-components |
| description | Code generation and conversion for Next.js 16 applications using Cache Components (PPR). Use when the user requests to "convert to use cacheComponents" at project or page level, create new Next.js pages/components with proper caching directives, split pages into cached and dynamic sections with Suspense boundaries, or work with Next.js 16's new caching model including use cache, cacheLife, Suspense, and anonymous Supabase clients for cache-friendly data fetching. |
Next.js 16 Cache Components
Generate and convert Next.js 16 code using Cache Components (Partial Prerendering). Handle proper use cache directives, Suspense boundaries, anonymous Supabase clients, and migration from old route segment configs.
Core Principles
- Dynamic by default: All routes are dynamic unless explicitly cached with
use cache
- Fine-grained control: Cache at page, component, or function level
- No runtime APIs in cached scopes: Cached code cannot use
cookies(), headers(), or runtime searchParams
- Anonymous clients for caching: Use Supabase anonymous clients (no cookies) in cached scopes
- Suspense for dynamic content: Wrap runtime-dependent code in Suspense boundaries
Anonymous Supabase Client Pattern
Critical: Cached components cannot use standard Supabase clients that rely on cookies. Always use anonymous clients in cached scopes.
Create the anonymous client:
import type { Database } from "@/types/database";
import { createServerClient } from "@supabase/ssr";
export const createSupabaseAnonServerClient = async () => {
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
{
cookies: {
getAll() {
return [];
},
setAll(cookiesToSet) {
},
},
auth: {
autoRefreshToken: false,
persistSession: false,
detectSessionInUrl: false,
},
},
);
};
Use in cached pages/components:
export default async function HomePage() {
'use cache';
cacheLife('hours');
const supabase = await createSupabaseAnonServerClient();
const { data } = await supabase.from('products').select('*');
return <div>{/* render */}</div>;
}
Cache Directives
Standard Cache
"use cache";
Use for pages/components without params/searchParams that should be cached.
Private Cache
"use cache: private";
Required when using params or other request-specific data. Creates per-request cache keys.
Cache Lifetime
import { cacheLife } from "next/cache";
("use cache");
cacheLife("days");
cacheLife("hours");
cacheLife("minutes");
Cache Tags
import { cacheTag } from "next/cache";
("use cache");
cacheTag("products");
cacheTag("category-electronics");
Use for targeted cache invalidation with updateTag() or revalidateTag().
Code Generation Patterns
Pattern 1: Fully Cached Page
Generate when: Content is static or rarely changes, no user-specific data.
import { cacheLife } from 'next/cache';
import { createSupabaseAnonServerClient } from '@/supabase-clients/anon/createSupabaseAnonServerClient';
export default async function ProductsPage() {
'use cache';
cacheLife('hours');
const supabase = await createSupabaseAnonServerClient();
const { data: products } = await supabase
.from('products')
.select('*')
.limit(20);
return (
<div>
<h1>Products</h1>
<div className="grid">
{products?.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
</div>
);
}
Pattern 2: Page with Params (Private Cache)
Generate when: Using route params or dynamic segments.
import { cacheLife } from 'next/cache';
import { notFound } from 'next/navigation';
import { createSupabaseAnonServerClient } from '@/supabase-clients/anon/createSupabaseAnonServerClient';
interface CategoryPageProps {
params: Promise<{ slug: string }>;
}
export default async function CategoryPage({ params }: CategoryPageProps) {
'use cache: private';
cacheLife('minutes');
const { slug } = await params;
const supabase = await createSupabaseAnonServerClient();
const { data: category } = await supabase
.from('categories')
.select('*')
.eq('slug', slug)
.single();
if (!category) {
notFound();
}
return (
<div>
<h1>{category.name}</h1>
<p>{category.description}</p>
</div>
);
}
Pattern 3: Mixed Static/Dynamic Page
Generate when: Page has both cacheable and dynamic content.
import { Suspense } from 'react';
import { cacheLife, cacheTag } from 'next/cache';
import { cookies } from 'next/headers';
import { createSupabaseAnonServerClient } from '@/supabase-clients/anon/createSupabaseAnonServerClient';
export default function DashboardPage() {
return (
<div>
<StaticHeader />
<Suspense fallback={<UserSkeleton />}>
<UserProfile />
</Suspense>
<Suspense fallback={<ProductsSkeleton />}>
<RecentProducts />
</Suspense>
</div>
);
}
async function StaticHeader() {
'use cache';
cacheLife('hours');
cacheTag('header');
const supabase = await createSupabaseAnonServerClient();
const { data: categories } = await supabase
.from('categories')
.select('name, slug');
return (
<nav>
{categories?.map(cat => (
<a key={cat.slug} href={`/category/${cat.slug}`}>
{cat.name}
</a>
))}
</nav>
);
}
async function UserProfile() {
const cookieStore = await cookies();
const session = cookieStore.get('session')?.value;
return <div>{/* User profile */}</div>;
}
async function RecentProducts() {
const supabase = await createSupabaseAnonServerClient();
const { data: products } = await supabase
.from('products')
.select('*')
.order('created_at', { ascending: false })
.limit(5);
return <div>{/* Products list */}</div>;
}
Pattern 4: Cached Utility Functions
Generate when: Data fetching logic should be reusable and cached.
import { cacheLife, cacheTag } from "next/cache";
import { createSupabaseAnonServerClient } from "@/supabase-clients/anon/createSupabaseAnonServerClient";
export async function getCategories() {
"use cache";
cacheLife("hours");
cacheTag("categories");
const supabase = await createSupabaseAnonServerClient();
const { data } = await supabase.from("categories").select("*");
return data ?? [];
}
export async function getTrendingProducts(limit: number = 10) {
"use cache: private";
cacheLife("minutes");
cacheTag("trending");
const supabase = await createSupabaseAnonServerClient();
const { data } = await supabase.rpc("get_trending_products", {
limit_arg: limit,
});
return data ?? [];
}
Pattern 5: SearchParams with Prop Passing
Generate when: Need to use searchParams but want to cache static parts.
import { Suspense } from 'react';
interface PageProps {
searchParams: Promise<{ sort?: string; filter?: string }>;
}
export default function ProductsPage({ searchParams }: PageProps) {
return (
<div>
<h1>Products</h1>
<StaticCategories />
<Suspense fallback={<ProductsSkeleton />}>
<FilteredProducts
sortPromise={searchParams.then(sp => sp.sort)}
filterPromise={searchParams.then(sp => sp.filter)}
/>
</Suspense>
</div>
);
}
async function FilteredProducts({
sortPromise,
filterPromise
}: {
sortPromise: Promise<string | undefined>;
filterPromise: Promise<string | undefined>;
}) {
const sort = await sortPromise;
const filter = await filterPromise;
const supabase = await createSupabaseAnonServerClient();
let query = supabase.from('products').select('*');
if (filter) {
query = query.eq('category', filter);
}
if (sort) {
query = query.order(sort as any);
}
const { data: products } = await query;
return <div>{/* Render products */}</div>;
}
Conversion Process
When converting existing Next.js code to Cache Components:
Step 1: Enable in Config
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
};
export default nextConfig;
Step 2: Create Anonymous Supabase Client
If using Supabase, create the anonymous client (see pattern above).
Step 3: Analyze and Convert
For pages with old route segment configs:
export const dynamic = 'force-static';
export const revalidate = 3600;
export default async function Page() {
const data = await fetch('...');
return <div>...</div>;
}
import { cacheLife } from 'next/cache';
export default async function Page() {
'use cache';
cacheLife('hours');
const data = await fetch('...');
return <div>...</div>;
}
For pages with cookies/sessions:
export default async function Page() {
const supabase = createClient();
const { data } = await supabase.from('products').select('*');
return <div>...</div>;
}
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<CachedProducts /> {/* Use anon client */}
<Suspense fallback={<UserSkeleton />}>
<UserSection /> {/* Use regular client with cookies */}
</Suspense>
</div>
);
}
async function CachedProducts() {
'use cache';
cacheLife('hours');
const supabase = await createSupabaseAnonServerClient();
const { data } = await supabase.from('products').select('*');
return <div>...</div>;
}
async function UserSection() {
const supabase = createClient();
return <div>...</div>;
}
Step 4: Add Cache Invalidation
"use server";
import { updateTag } from "next/cache";
export async function createProduct(formData: FormData) {
const supabase = await createSupabaseAnonServerClient();
await supabase.from("products").insert({
name: formData.get("name"),
});
updateTag("products");
}
Route Handlers
Static Route Handler
export async function GET() {
return Response.json({ status: "ok", version: "1.0.0" });
}
Cached Data Route Handler
import { cacheLife } from "next/cache";
export async function GET() {
const products = await getProducts();
return Response.json(products);
}
async function getProducts() {
"use cache";
cacheLife("hours");
const supabase = await createSupabaseAnonServerClient();
const { data } = await supabase.from("products").select("*");
return data ?? [];
}
Common Issues and Solutions
"Uncached data was accessed outside of "
Solution: Wrap dynamic component in Suspense or add use cache to make it cached.
"Cannot use cookies() in cached component"
Solution: Use anonymous Supabase client or move cookies() access to Suspense boundary.
Params not working
Solution: Use 'use cache: private' and await params.
Cache not updating
Solution: Add cacheTag() and call updateTag() or revalidateTag() in mutations.
Additional Resources
For detailed patterns and examples, reference:
references/patterns.md - Common code patterns for all scenarios
references/migration.md - Step-by-step migration guide from old Next.js code
Key Reminders
- Always use anonymous Supabase client in cached scopes
- Use
'use cache: private' with params
- Await params:
const { slug } = await params
- Wrap runtime APIs in Suspense
- Tag cached data for invalidation
- Choose appropriate cacheLife based on update frequency