Next.js Data Rendering Expert
next-data-rendering
Use when implementing rendering strategies like SSG, SSR, ISR, CSR, or managing caching and revalidation. Invoke for static generation, server-side rendering, incremental static regeneration, partial prerendering, or cache optimization.
SKILL.md
Full skill instructions
Next.js Data Rendering Expert
Purpose
Expert knowledge of Next.js rendering strategies and data fetching patterns. Covers SSG (Static Site Generation), SSR (Server-Side Rendering), ISR (Incremental Static Regeneration), CSR (Client-Side Rendering), caching strategies, and revalidation.
When to Use
Invoke this skill when:
- Choosing rendering strategies for pages
- Implementing caching strategies
- Setting up ISR with revalidation
- Implementing Partial Prerendering (PPR)
- Optimizing page load performance
- Managing cache invalidation
- Implementing streaming and Suspense
- Debugging caching or stale data issues
- Balancing static and dynamic content
Documentation Available
Location: /Users/zach/Documents/cc-skills/docs/next/
Coverage (~60 files):
-
Rendering Strategies:
- Static Site Generation (SSG)
- Server-Side Rendering (SSR)
- Incremental Static Regeneration (ISR)
- Client-Side Rendering (CSR)
- Partial Prerendering (PPR - experimental)
-
Caching:
- Request memoization
- Data cache
- Full route cache
- Router cache
- Cache configuration
-
Revalidation:
- Time-based revalidation
- On-demand revalidation
- Tag-based revalidation
Related Skills
- next-app-router: For app/ directory implementation
- next-pages-router: For pages/ directory implementation
- next-config-optimization: For build optimization
Best Practices
- Default to static - Use SSG for best performance
- Use ISR for freshness - Balance static and dynamic
- Implement proper cache headers - Control CDN caching
- Use tag-based revalidation - Granular cache invalidation
- Leverage streaming - Show content progressively
- Minimize dynamic rendering - Static is faster
- Use Partial Prerendering - Mix static and dynamic
- Cache at multiple levels - Request, data, route, router
- Set appropriate revalidation times - Based on data freshness needs
Examples
Example 1: Static Site Generation (SSG)
Question: "How do I implement SSG with Next.js?"
Response:
// App Router (app/products/page.tsx)
export const dynamic = 'force-static'; // Explicitly static (default)
async function ProductsPage() {
// Fetched at build time
const products = await fetch('https://api.example.com/products', {
cache: 'force-cache', // SSG (default)
}).then(r => r.json());
return (
<div>
<h1>Products (Built at: {new Date().toISOString()})</h1>
<ul>
{products.map(p => (
<li key={p.id}>{p.name}</li>
))}
</ul>
</div>
);
}
export default ProductsPage;
// Pages Router (pages/products.tsx)
import { GetStaticProps } from 'next';
export const getStaticProps: GetStaticProps = async () => {
const products = await fetch('https://api.example.com/products')
.then(r => r.json());
return {
props: { products },
};
};
Benefits:
- Fastest performance (served from CDN)
- SEO-friendly (pre-rendered HTML)
- Low server load
- Works with static hosting
Use Cases:
- Marketing pages
- Blog posts
- Documentation
- Product catalogs (with ISR)
References:
- See:
docs/next/app/building-your-application/rendering/server-components/
Example 2: Incremental Static Regeneration (ISR)
Question: "How do I implement ISR for fresh data?"
Response:
// App Router (app/products/page.tsx)
export const revalidate = 3600; // Revalidate every hour
async function ProductsPage() {
const products = await fetch('https://api.example.com/products', {
next: { revalidate: 3600 }, // Per-request revalidation
}).then(r => r.json());
return (
<div>
<h1>Products</h1>
<p>Last updated: {new Date().toISOString()}</p>
<ul>
{products.map(p => (
<li key={p.id}>{p.name} - ${p.price}</li>
))}
</ul>
</div>
);
}
// Different revalidation times for different requests
async function HomePage() {
const [products, posts] = await Promise.all([
fetch('https://api.example.com/products', {
next: { revalidate: 3600 }, // 1 hour
}).then(r => r.json()),
fetch('https://api.example.com/posts', {
next: { revalidate: 60 }, // 1 minute
}).then(r => r.json()),
]);
return <div>{/* Content */}</div>;
}
// Pages Router (pages/products.tsx)
export const getStaticProps: GetStaticProps = async () => {
const products = await fetch('https://api.example.com/products')
.then(r => r.json());
return {
props: { products },
revalidate: 3600, // Revalidate every hour
};
};
// On-demand revalidation (App Router)
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const secret = request.nextUrl.searchParams.get('secret');
if (secret !== process.env.REVALIDATE_SECRET) {
return Response.json({ message: 'Invalid secret' }, { status: 401 });
}
// Revalidate specific path
revalidatePath('/products');
// Or revalidate by tag
revalidateTag('products');
return Response.json({ revalidated: true, now: Date.now() });
}
// Tag-based revalidation
async function getProducts() {
const products = await fetch('https://api.example.com/products', {
next: { tags: ['products'] }, // Tag for revalidation
}).then(r => r.json());
return products;
}
ISR Workflow:
- Build time: Generate static pages
- Request: Serve stale page instantly
- Background: Regenerate if expired
- Next request: Serve fresh page
Benefits:
- Static performance
- Always fresh data
- Automatic regeneration
- No rebuild needed
References:
- See:
docs/next/app/building-your-application/data-fetching/fetching-caching-and-revalidating/
Example 3: Server-Side Rendering (SSR)
Question: "How do I implement SSR for dynamic content?"
Response:
// App Router (app/dashboard/page.tsx)
export const dynamic = 'force-dynamic'; // Force SSR
export const revalidate = 0; // Don't cache
async function DashboardPage() {
// Fetched on every request
const user = await fetch('https://api.example.com/user', {
cache: 'no-store', // SSR
headers: {
Cookie: cookies().toString(), // Access request cookies
},
}).then(r => r.json());
const stats = await fetch('https://api.example.com/stats', {
cache: 'no-store',
}).then(r => r.json());
return (
<div>
<h1>Welcome, {user.name}</h1>
<p>Total Sales: ${stats.totalSales}</p>
<p>Generated at: {new Date().toISOString()}</p>
</div>
);
}
// Access request data
import { cookies, headers } from 'next/headers';
async function ProfilePage() {
const cookieStore = cookies();
const token = cookieStore.get('token');
const headersList = headers();
const userAgent = headersList.get('user-agent');
// Fetch based on request data
const user = await fetch('https://api.example.com/user', {
cache: 'no-store',
headers: {
Authorization: `Bearer ${token?.value}`,
},
}).then(r => r.json());
return <div>Hello, {user.name}</div>;
}
// Pages Router (pages/dashboard.tsx)
import { GetServerSideProps } from 'next';
export const getServerSideProps: GetServerSideProps = async (context) => {
const { req, res } = context;
const user = await fetch('https://api.example.com/user', {
headers: {
Cookie: req.headers.cookie || '',
},
}).then(r => r.json());
// Set cache headers
res.setHeader(
'Cache-Control',
'public, s-maxage=10, stale-while-revalidate=59'
);
return {
props: { user },
};
};
When to Use SSR:
- User-specific content
- Highly dynamic data
- Request-dependent data (cookies, headers)
- Real-time data
Drawbacks:
- Slower than static
- Higher server load
- No CDN caching (by default)
References:
- See:
docs/next/app/building-your-application/rendering/server-components/
Example 4: Client-Side Rendering (CSR)
Question: "How do I implement CSR for interactive data?"
Response:
// App Router (app/products/product-list.tsx)
'use client';
import { useState, useEffect } from 'react';
import useSWR from 'swr';
// Traditional useEffect approach
export function ProductListEffect() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/products')
.then(r => r.json())
.then(data => {
setProducts(data);
setLoading(false);
});
}, []);
if (loading) return <div>Loading...</div>;
return (
<ul>
{products.map(p => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
// SWR approach (recommended)
const fetcher = (url: string) => fetch(url).then(r => r.json());
export function ProductListSWR() {
const { data, error, isLoading, mutate } = useSWR('/api/products', fetcher, {
revalidateOnFocus: true,
revalidateOnReconnect: true,
refreshInterval: 30000, // Poll every 30s
});
if (error) return <div>Failed to load</div>;
if (isLoading) return <div>Loading...</div>;
return (
<div>
<button onClick={() => mutate()}>Refresh</button>
<ul>
{data.map(p => (
<li key={p.id}>{p.name}</li>
))}
</ul>
</div>
);
}
// React Query approach
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
export function ProductListQuery() {
const queryClient = useQueryClient();
const { data, isLoading, error } = useQuery({
queryKey: ['products'],
queryFn: () => fetch('/api/products').then(r => r.json()),
staleTime: 5000, // Consider fresh for 5s
cacheTime: 10 * 60 * 1000, // Cache for 10 minutes
});
const mutation = useMutation({
mutationFn: (newProduct) =>
fetch('/api/products', {
method: 'POST',
body: JSON.stringify(newProduct),
}).then(r => r.json()),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error loading products</div>;
return <ul>{/* Products */}</ul>;
}
When to Use CSR:
- User interactions
- Real-time updates
- Personalized content
- Non-SEO content
Benefits:
- Interactive
- No server load
- Real-time updates
- Automatic revalidation (with SWR/React Query)
References:
- See:
docs/next/app/building-your-application/rendering/client-components/
Example 5: Partial Prerendering (PPR)
Question: "How do I use Partial Prerendering?"
Response:
// next.config.js
module.exports = {
experimental: {
ppr: true, // Enable PPR
},
};
// app/products/page.tsx
import { Suspense } from 'react';
// Static shell
export default function ProductsPage() {
return (
<div>
<h1>Products</h1>
<p>This header is static</p>
{/* Dynamic content with Suspense */}
<Suspense fallback={<ProductsSkeleton />}>
<ProductList />
</Suspense>
{/* Another dynamic section */}
<Suspense fallback={<ReviewsSkeleton />}>
<RecentReviews />
</Suspense>
<footer>This footer is static</footer>
</div>
);
}
// Dynamic components (Server Components)
async function ProductList() {
const products = await fetch('https://api.example.com/products', {
cache: 'no-store', // Dynamic
}).then(r => r.json());
return (
<ul>
{products.map(p => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
async function RecentReviews() {
const reviews = await fetch('https://api.example.com/reviews', {
cache: 'no-store',
}).then(r => r.json());
return (
<div>
{reviews.map(r => (
<div key={r.id}>{r.content}</div>
))}
</div>
);
}
PPR Benefits:
- Instant static shell
- Streaming dynamic content
- Best of both worlds
- SEO-friendly
- Fast perceived performance
How it Works:
- Serve static shell instantly
- Stream dynamic content as it loads
- Replace Suspense fallbacks progressively
References:
- See:
docs/next/app/api-reference/next-config-js/partial-prerendering/
Common Patterns
Cache Configuration
// Opt into caching
fetch('https://api.example.com/data', {
cache: 'force-cache', // Default
});
// Opt out of caching
fetch('https://api.example.com/data', {
cache: 'no-store',
});
// ISR
fetch('https://api.example.com/data', {
next: { revalidate: 3600 },
});
// Tag-based caching
fetch('https://api.example.com/data', {
next: { tags: ['products'] },
});
Streaming with Suspense
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<h1>Page Title</h1>
<Suspense fallback={<Skeleton />}>
<AsyncComponent />
</Suspense>
</div>
);
}
Revalidation Strategies
// Time-based (page-level)
export const revalidate = 3600;
// Time-based (request-level)
fetch(url, { next: { revalidate: 3600 } });
// On-demand (path)
revalidatePath('/products');
// On-demand (tag)
revalidateTag('products');
Search Helpers
# Find rendering docs
grep -r "SSG\|SSR\|ISR\|rendering" /Users/zach/Documents/cc-skills/docs/next/
# Find caching docs
grep -r "cache\|revalidate\|fetch" /Users/zach/Documents/cc-skills/docs/next/
# Find streaming docs
grep -r "streaming\|Suspense\|PPR" /Users/zach/Documents/cc-skills/docs/next/
# List rendering files
ls /Users/zach/Documents/cc-skills/docs/next/app/building-your-application/rendering/
Common Errors
-
Data is stale: Cache not revalidating
- Solution: Set appropriate revalidate time or use
cache: 'no-store'
- Solution: Set appropriate revalidate time or use
-
Page is slow: Using SSR unnecessarily
- Solution: Switch to ISR with revalidation
-
Build fails: Too many static pages
- Solution: Use
fallback: 'blocking'in getStaticPaths
- Solution: Use
-
Cache not working: Dynamic functions in static page
- Solution: Remove cookies(), headers(), or searchParams access
Performance Comparison
| Strategy | Speed | Freshness | Server Load | SEO | Use Case |
|---|---|---|---|---|---|
| SSG | Fastest | Static | None | Best | Marketing, blogs |
| ISR | Fast | Fresh | Low | Best | Product pages |
| SSR | Slow | Real-time | High | Good | Dashboards |
| CSR | Instant shell | Real-time | None | Poor | User interactions |
| PPR | Fast | Mixed | Low | Best | E-commerce |
Caching Layers
- Request Memoization: Dedupe same requests in single render
- Data Cache: Persist fetch results across requests
- Full Route Cache: Pre-rendered HTML + RSC payload
- Router Cache: Client-side cached routes
Notes
- Documentation covers Next.js 13-14
- App Router defaults to SSG
- fetch() is extended with caching options
- Partial Prerendering is experimental (Next.js 14+)
- ISR works with static hosting (Vercel, Netlify)
- Tag-based revalidation enables granular cache control
- File paths reference local documentation cache
- For latest updates, check https://nextjs.org/docs/app/building-your-application/rendering
