Skip to main content

nextjs-reviewer

Review Next.js App Router code for optimal Partial Prerendering (PPR), caching strategy, Suspense boundaries, and React Query integration. Ensure adherence to Next.js 16+ Cache Components best practices.

Quellinformationen

Repository
jaredpalmer/claude-plugins
Letzte Quellaktivität
1. Dezember 2025 um 20:57
Erkannte Sprache von SKILL.md
Englisch
Sterne
6
Forks
0

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
nextjs-reviewer
description
Review Next.js App Router code for optimal Partial Prerendering (PPR), caching strategy, Suspense boundaries, and React Query integration. Ensure adherence to Next.js 16+ Cache Components best practices.
# Next.js PPR & Caching Code Review Skill ## Purpose Review Next.js App Router code for optimal Partial Prerendering (PPR), caching strategy, Suspense boundaries, and React Query integration. Ensure adherence to Next.js 16+ Cache Components best practices. > **Documentation Version**: Based on Next.js 16.0.4 official documentation > **Last Updated**: 2025-11-25 > **Source**: https://nextjs.org/docs/app/getting-started/partial-prerendering ## When to Use - Before creating pull requests with Next.js components - When implementing new data-fetching features - During performance optimization reviews - When adding or modifying Suspense boundaries - After implementing caching strategies ## Prerequisites - Next.js 16+ with `cacheComponents: true` in next.config - App Router (not Pages Router) - Understanding of Server Components vs Client Components --- ## Understanding the Two Cache Systems > **📖 Reference**: [Cache Components - With runtime data](https://nextjs.org/docs/app/getting-started/partial-prerendering#with-runtime-data) Before reviewing code, understand these two **completely different** caching mechanisms: | Concept | React `cache()` | `'use cache'` directive | |---------|-----------------|-------------------------| | **Import** | `import { cache } from 'react'` | Directive: `'use cache'` | | **Scope** | **Same-REQUEST** deduplication | **Cross-REQUEST** caching | | **Duration** | Single render pass only | Minutes / hours / days | | **Use Case** | `getCurrentUser()` called 5x = 1 actual call | Data cached for all users | | **Works with cookies()** | ✅ Yes (wraps the function) | ❌ No (use `'use cache: private'`) | ### The Architecture Layers ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ LAYER 1: Layout/Page (STATIC SHELL) │ │ ─────────────────────────────────────────────────────────────────────── │ │ • NO cookies(), NO headers(), NO runtime data │ │ • Prerendered at build time → instant delivery │ │ • Contains <Suspense> boundaries as deep as possible │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ LAYER 2: Auth Boundary (DYNAMIC - inside Suspense) │ │ ─────────────────────────────────────────────────────────────────────── │ │ • Calls cookies() to get session token │ │ • Uses getCurrentUser() wrapped with React cache() for dedup │ │ • Handles redirect('/login') if not authenticated │ │ • Passes accessToken DOWN to cached components as prop │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ LAYER 3: Cached Data (CACHED - 'use cache' with token as key) │ │ ─────────────────────────────────────────────────────────────────────── │ │ • Receives accessToken as PROP (automatically becomes cache key) │ │ • Uses 'use cache' + cacheLife() + cacheTag() │ │ • Fetches user-specific data using the token │ │ • Cached PER-USER across multiple requests │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### Complete Auth + Caching Implementation **Step 1: Auth Utilities (`auth/server.ts`)** ```typescript import { cache } from 'react'; import { cookies } from 'next/headers'; import { redirect } from 'next/navigation'; // Internal: Read session from cookie (CANNOT be cached - runtime data) async function getSessionFromCookie() { const cookieStore = await cookies(); const session = cookieStore.get('github_session')?.value; return session ? decrypt(session) : null; } // ✅ Wrapped with React cache() for SAME-REQUEST deduplication // If layout + page + 10 components call this = 1 actual cookie read export const getCurrentUser = cache(async () => { const session = await getSessionFromCookie(); if (!session) return null; return { accessToken: session.githubToken, userId: session.githubId, userName: session.userName, }; }); // ✅ Auth guard - redirects if not logged in export async function requireAuth() { const user = await getCurrentUser(); if (!user) { redirect('/login'); } return user; } ``` **Step 2: Layout (STATIC SHELL - no runtime data)** ```typescript // app/layout.tsx export default function RootLayout({ children }: { children: React.ReactNode }) { // ⚠️ NO cookies() here! Layout stays in static shell. return ( <html> <body> <StaticHeader /> {/* ✅ Part of static shell */} <StaticSidebar /> {/* ✅ Part of static shell */} {children} <StaticFooter /> {/* ✅ Part of static shell */} </body> </html> ); } ``` **Step 3: Page with Suspense Boundaries (as deep as possible)** ```typescript // app/pulls/page.tsx import { Suspense } from 'react'; export default function PullsPage() { // ✅ Page itself is STATIC - no runtime data access here return ( <div> <h1>Pull Requests</h1> {/* ✅ Static shell */} {/* ✅ Suspense boundary as DEEP as possible */} <Suspense fallback={<PullsSkeleton />}> <AuthenticatedPullsList /> </Suspense> </div> ); } ``` **Step 4: Auth Boundary Component (DYNAMIC)** ```typescript // components/authenticated-pulls-list.tsx import { requireAuth } from '@/auth/server'; // ⚠️ This component is DYNAMIC - accesses cookies via requireAuth // ⚠️ MUST be wrapped in <Suspense> at usage site export async function AuthenticatedPullsList() { // Step 1: Auth check (reads cookies, may redirect) const user = await requireAuth(); // Step 2: Pass token to CACHED component (token = cache key) return <PullsListCached accessToken={user.accessToken} />; } ``` **Step 5: Cached Data Component** ```typescript // components/pulls-list-cached.tsx import { cacheLife, cacheTag } from 'next/cache'; // ✅ This component is CACHED across requests // ✅ accessToken is part of cache key - each user gets own cache async function PullsListCached({ accessToken }: { accessToken: string }) { 'use cache'; cacheLife('minutes'); // 5 min stale, 1 min revalidate cacheTag('user-pulls'); // For on-demand invalidation // This fetch is cached per-user (keyed by accessToken prop) const client = createGitHubClient(accessToken); const pulls = await client.pulls.list(); return ( <ul> {pulls.map(pr => <PullRequestItem key={pr.id} pr={pr} />)} </ul> ); } ``` ### Why This Pattern is Optimal | Benefit | How It's Achieved | |---------|-------------------| | **Maximum static shell** | Layout, headers, titles prerendered instantly | | **Suspense as deep as possible** | Only data sections stream; everything else instant | | **No duplicate cookie reads** | `getCurrentUser()` with React `cache()` = 1 read per request | | **Cross-request caching** | `'use cache'` with token key = per-user cache reuse | | **Cache isolation** | Token as prop = automatic per-user cache keys | ### Critical Insight: Where Auth Happens ```typescript // ❌ WRONG - Auth in layout blocks entire layout from prerendering export default async function Layout({ children }) { const user = await getCurrentUser(); // cookies() blocks prerender! return <div>{children}</div>; } // ✅ CORRECT - Layout is static, auth is inside page's Suspense export default function Layout({ children }) { return ( <div> <StaticNav /> {children} {/* Pages put auth inside their own Suspense */} </div> ); } ``` ### Decision Matrix: Which Cache to Use | What You're Doing | Which Cache | Why | |-------------------|-------------|-----| | `getCurrentUser()` - reading cookies | React `cache()` | Same-request dedup; can't cache cookies cross-request | | `getGitHubClient(token)` - creating client | React `cache()` | Same-request dedup; reuse client instance | | `fetchUserRepos(token)` - API call with token | `'use cache'` | Cross-request cache; token is cache key | | `fetchPublicRepo(owner, repo)` - public data | `'use cache'` | Cross-request cache; no auth needed | | `fetchUserDashboard()` - needs cookies directly | `'use cache: private'` | Cross-request with cookie access | --- ## Review Checklist ### 1. PPR Pattern Implementation > **📖 Reference**: [Cache Components](https://nextjs.org/docs/app/getting-started/partial-prerendering) **The Core Concept:** Cache Components lets you mix **static**, **cached**, and **dynamic** content in a single route: | Content Type | When Used | How to Handle | |--------------|-----------|---------------| | **Static** | Synchronous I/O, pure computations | Auto-prerendered into static shell | | **Cached** | Dynamic data without runtime context | Use `'use cache'` directive | | **Dynamic** | Needs cookies, headers, searchParams | Wrap in `<Suspense>` boundaries | **✅ CORRECT Pattern (Public/Shared Data):** ```typescript // Outer component - accesses runtime data (stays dynamic) export async function DataSection() { const user = await getCurrentUser(); // accesses cookies if (!user?.accessToken) redirect('/login'); return <DataSectionCached accessToken={user.accessToken} />; } // Inner component - cached with 'use cache' async function DataSectionCached({ accessToken }: { accessToken: string }) { 'use cache'; cacheLife('minutes'); const client = getCachedAuthenticatedClient(accessToken); const data = await fetchData(client); return <UI data={data} />; } // ⚠️ CRITICAL: Usage site MUST wrap in Suspense // app/page.tsx export default function Page() { return ( <Suspense fallback={<DataSkeleton />}> <DataSection /> </Suspense> ); } ``` **❌ INCORRECT Pattern:** ```typescript // ❌ Auth check blocks everything from being cached export async function DataSection() { const user = await getCurrentUser(); // accesses cookies - blocks caching const client = getCachedAuthenticatedClient(user.accessToken); const data = await fetchData(client); // this could be cached but isn't return <UI data={data} />; } ``` **Check for:** - [ ] Runtime data access (cookies, headers, searchParams) isolated in outer wrapper - [ ] Data fetching moved to inner cached component - [ ] `'use cache'` directive at top of cached function/component - [ ] `cacheLife()` called with appropriate duration - [ ] Cache key includes all varying parameters (passed as props) - [ ] **Outer component wrapped in `<Suspense>` at usage site** --- ### 1.5 PPR Pattern with Personalized Data (`use cache: private`) > **📖 Reference**: [`use cache: private` directive](https://nextjs.org/docs/app/api-reference/directives/use-cache-private) **When to Use**: For **user-specific** data where each user needs their own cache entry (dashboards, feeds, personalized recommendations). **✅ CORRECT Pattern:** ```typescript import { cookies } from 'next/headers'; import { cacheLife, cacheTag } from 'next/cache'; import { Suspense } from 'react'; // Usage - MUST wrap in Suspense (not prerendered) export default function Page() { return ( <Suspense fallback={<DashboardSkeleton />}> <UserDashboard /> </Suspense> ); } // Single function - no split needed with private cache! async function UserDashboard() { 'use cache: private'; cacheLife({ stale: 60 }); // Minimum 30s required for runtime prefetch // Can access cookies directly const session = await cookies(); const userId = session.get('userId')?.value; const data = await fetchUserSpecificData(userId); return <Dashboard data={data} />; } ``` **Real-World Example (GitHub-style):** ```typescript // User's personalized pull request dashboard async function MyPullsPage() { 'use cache: private'; cacheLife('minutes'); // 5 min stale, 1 min revalidate const session = await cookies(); const userId = session.get('userId')?.value; const myPrs = await db.pulls.findMany({ where: { OR: [ { authorId: userId }, { assignees: { some: { id: userId } } }, ], }, }); return <DashboardTable items={myPrs} />; } ``` **Comparison: Public vs Private Caching** | Feature | `'use cache'` (Public) | `'use cache: private'` (Private) |
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen