Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Collection of 146 agent skills across 72 plugins for full-stack development, orchestration, and automation.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
SKILL.md
1---2name: nextjs-app-router-patterns3description: Master Next.js 14+ App Router with Server Components, streaming, parallel routes, and advanced data fetching. Use when building Next.js applications, implementing SSR/SSG, or optimizing React Server Components.4---56# Next.js App Router Patterns78Comprehensive patterns for Next.js 14+ App Router architecture, Server Components, and modern full-stack React development.910## When to Use This Skill1112- Building new Next.js applications with App Router13- Migrating from Pages Router to App Router14- Implementing Server Components and streaming15- Setting up parallel and intercepting routes16- Optimizing data fetching and caching17- Building full-stack features with Server Actions1819## Core Concepts2021### 1. Rendering Modes2223| Mode | Where | When to Use |24| --------------------- | ------------ | ----------------------------------------- |25| **Server Components** | Server only | Data fetching, heavy computation, secrets |26| **Client Components** | Browser | Interactivity, hooks, browser APIs |27| **Static** | Build time | Content that rarely changes |28| **Dynamic** | Request time | Personalized or real-time data |29| **Streaming** | Progressive | Large pages, slow data sources |3031### 2. File Conventions3233```34app/35├── layout.tsx # Shared UI wrapper36├── page.tsx # Route UI37├── loading.tsx # Loading UI (Suspense)38├── error.tsx # Error boundary39├── not-found.tsx # 404 UI40├── route.ts # API endpoint41├── template.tsx # Re-mounted layout42├── default.tsx # Parallel route fallback43└── opengraph-image.tsx # OG image generation44```4546## Quick Start4748```typescript49// app/layout.tsx50import { Inter } from 'next/font/google'51import { Providers } from './providers'5253const inter = Inter({ subsets: ['latin'] })5455export const metadata = {56title: { default: 'My App', template: '%s | My App' },57description: 'Built with Next.js App Router',58}5960export default function RootLayout({61children,62}: {63children: React.ReactNode64}) {65return (66<html lang="en" suppressHydrationWarning>67<body className={inter.className}>68<Providers>{children}</Providers>69</body>70</html>71)72}7374// app/page.tsx - Server Component by default75async function getProducts() {76const res = await fetch('https://api.example.com/products', {77next: { revalidate: 3600 }, // ISR: revalidate every hour78})79return res.json()80}8182export default async function HomePage() {83const products = await getProducts()8485return (86<main>87<h1>Products</h1>88<ProductGrid products={products} />89</main>90)91}92```9394## Patterns9596### Pattern 1: Server Components with Data Fetching9798```typescript99// app/products/page.tsx100import { Suspense } from 'react'101import { ProductList, ProductListSkeleton } from '@/components/products'102import { FilterSidebar } from '@/components/filters'103104interface SearchParams {105category?: string106sort?: 'price' | 'name' | 'date'107page?: string108}109110export default async function ProductsPage({111searchParams,112}: {113searchParams: Promise<SearchParams>114}) {115const params = await searchParams116117return (118<div className="flex gap-8">119<FilterSidebar />120<Suspense121key={JSON.stringify(params)}122fallback={<ProductListSkeleton />}123>124<ProductList125category={params.category}126sort={params.sort}127page={Number(params.page) || 1}128/>129</Suspense>130</div>131)132}133134// components/products/ProductList.tsx - Server Component135async function getProducts(filters: ProductFilters) {136const res = await fetch(137`${process.env.API_URL}/products?${new URLSearchParams(filters)}`,138{ next: { tags: ['products'] } }139)140if (!res.ok) throw new Error('Failed to fetch products')141return res.json()142}143144export async function ProductList({ category, sort, page }: ProductFilters) {145const { products, totalPages } = await getProducts({ category, sort, page })146147return (148<div>149<div className="grid grid-cols-3 gap-4">150{products.map((product) => (151<ProductCard key={product.id} product={product} />152))}153</div>154<Pagination currentPage={page} totalPages={totalPages} />155</div>156)157}158```159160### Pattern 2: Client Components with 'use client'161162```typescript163// components/products/AddToCartButton.tsx164'use client'165166import { useState, useTransition } from 'react'167import { addToCart } from '@/app/actions/cart'168169export function AddToCartButton({ productId }: { productId: string }) {170const [isPending, startTransition] = useTransition()171const [error, setError] = useState<string | null>(null)172173const handleClick = () => {174setError(null)175startTransition(async () => {176const result = await addToCart(productId)177if (result.error) {178setError(result.error)179}180})181}182183return (184<div>185<button186onClick={handleClick}187disabled={isPending}188className="btn-primary"189>190{isPending ? 'Adding...' : 'Add to Cart'}191</button>192{error && <p className="text-red-500 text-sm">{error}</p>}193</div>194)195}196```197198### Pattern 3: Server Actions199200```typescript201// app/actions/cart.ts202"use server";203204import { revalidateTag } from "next/cache";205import { cookies } from "next/headers";206import { redirect } from "next/navigation";207208export async function addToCart(productId: string) {209const cookieStore = await cookies();210const sessionId = cookieStore.get("session")?.value;211212if (!sessionId) {213redirect("/login");214}215216try {217await db.cart.upsert({218where: { sessionId_productId: { sessionId, productId } },219update: { quantity: { increment: 1 } },220create: { sessionId, productId, quantity: 1 },221});222223revalidateTag("cart");224return { success: true };225} catch (error) {226return { error: "Failed to add item to cart" };227}228}229230export async function checkout(formData: FormData) {231const address = formData.get("address") as string;232const payment = formData.get("payment") as string;233234// Validate235if (!address || !payment) {236return { error: "Missing required fields" };237}238239// Process order240const order = await processOrder({ address, payment });241242// Redirect to confirmation243redirect(`/orders/${order.id}/confirmation`);244}245```246247### Pattern 4: Parallel Routes248249```typescript250// app/dashboard/layout.tsx251export default function DashboardLayout({252children,253analytics,254team,255}: {256children: React.ReactNode257analytics: React.ReactNode258team: React.ReactNode259}) {260return (261<div className="dashboard-grid">262<main>{children}</main>263<aside className="analytics-panel">{analytics}</aside>264<aside className="team-panel">{team}</aside>265</div>266)267}268269// app/dashboard/@analytics/page.tsx270export default async function AnalyticsSlot() {271const stats = await getAnalytics()272return <AnalyticsChart data={stats} />273}274275// app/dashboard/@analytics/loading.tsx276export default function AnalyticsLoading() {277return <ChartSkeleton />278}279280// app/dashboard/@team/page.tsx281export default async function TeamSlot() {282const members = await getTeamMembers()283return <TeamList members={members} />284}285```286287### Pattern 5: Intercepting Routes (Modal Pattern)288289```typescript290// File structure for photo modal291// app/292// ├── @modal/293// │ ├── (.)photos/[id]/page.tsx # Intercept294// │ └── default.tsx295// ├── photos/296// │ └── [id]/page.tsx # Full page297// └── layout.tsx298299// app/@modal/(.)photos/[id]/page.tsx300import { Modal } from '@/components/Modal'301import { PhotoDetail } from '@/components/PhotoDetail'302303export default async function PhotoModal({304params,305}: {306params: Promise<{ id: string }>307}) {308const { id } = await params309const photo = await getPhoto(id)310311return (312<Modal>313<PhotoDetail photo={photo} />314</Modal>315)316}317318// app/photos/[id]/page.tsx - Full page version319export default async function PhotoPage({320params,321}: {322params: Promise<{ id: string }>323}) {324const { id } = await params325const photo = await getPhoto(id)326327return (328<div className="photo-page">329<PhotoDetail photo={photo} />330<RelatedPhotos photoId={id} />331</div>332)333}334335// app/layout.tsx336export default function RootLayout({337children,338modal,339}: {340children: React.ReactNode341modal: React.ReactNode342}) {343return (344<html>345<body>346{children}347{modal}348</body>349</html>350)351}352```353354### Pattern 6: Streaming with Suspense355356```typescript357// app/product/[id]/page.tsx358import { Suspense } from 'react'359360export default async function ProductPage({361params,362}: {363params: Promise<{ id: string }>364}) {365const { id } = await params366367// This data loads first (blocking)368const product = await getProduct(id)369370return (371<div>372{/* Immediate render */}373<ProductHeader product={product} />374375{/* Stream in reviews */}376<Suspense fallback={<ReviewsSkeleton />}>377<Reviews productId={id} />378</Suspense>379380{/* Stream in recommendations */}381<Suspense fallback={<RecommendationsSkeleton />}>382<Recommendations productId={id} />383</Suspense>384</div>385)386}387388// These components fetch their own data389async function Reviews({ productId }: { productId: string }) {390const reviews = await getReviews(productId) // Slow API391return <ReviewList reviews={reviews} />392}393394async function Recommendations({ productId }: { productId: string }) {395const products = await getRecommendations(productId) // ML-based, slow396return <ProductCarousel products={products} />397}398```399400### Pattern 7: Route Handlers (API Routes)401402```typescript403// app/api/products/route.ts404import { NextRequest, NextResponse } from "next/server";405406export async function GET(request: NextRequest) {407const searchParams = request.nextUrl.searchParams;408const category = searchParams.get("category");409410const products = await db.product.findMany({411where: category ? { category } : undefined,412take: 20,413});414415return NextResponse.json(products);416}417418export async function POST(request: NextRequest) {419const body = await request.json();420421const product = await db.product.create({422data: body,423});424425return NextResponse.json(product, { status: 201 });426}427428// app/api/products/[id]/route.ts429export async function GET(430request: NextRequest,431{ params }: { params: Promise<{ id: string }> },432) {433const { id } = await params;434const product = await db.product.findUnique({ where: { id } });435436if (!product) {437return NextResponse.json({ error: "Product not found" }, { status: 404 });438}439440return NextResponse.json(product);441}442```443444### Pattern 8: Metadata and SEO445446```typescript447// app/products/[slug]/page.tsx448import { Metadata } from 'next'449import { notFound } from 'next/navigation'450451type Props = {452params: Promise<{ slug: string }>453}454455export async function generateMetadata({ params }: Props): Promise<Metadata> {456const { slug } = await params457const product = await getProduct(slug)458459if (!product) return {}460461return {462title: product.name,463description: product.description,464openGraph: {465title: product.name,466description: product.description,467images: [{ url: product.image, width: 1200, height: 630 }],468},469twitter: {470card: 'summary_large_image',471title: product.name,472description: product.description,473images: [product.image],474},475}476}477478export async function generateStaticParams() {479const products = await db.product.findMany({ select: { slug: true } })480return products.map((p) => ({ slug: p.slug }))481}482483export default async function ProductPage({ params }: Props) {484const { slug } = await params485const product = await getProduct(slug)486487if (!product) notFound()488489return <ProductDetail product={product} />490}491```492493## Caching Strategies494495### Data Cache496497```typescript498// No cache (always fresh)499fetch(url, { cache: "no-store" });500501// Cache forever (static)502fetch(url, { cache: "force-cache" });503504// ISR - revalidate after 60 seconds505fetch(url, { next: { revalidate: 60 } });506507// Tag-based invalidation508fetch(url, { next: { tags: ["products"] } });509510// Invalidate via Server Action511("use server");512import { revalidateTag, revalidatePath } from "next/cache";513514export async function updateProduct(id: string, data: ProductData) {515await db.product.update({ where: { id }, data });516revalidateTag("products");517revalidatePath("/products");518}519```520521## Best Practices522523### Do's524525- **Start with Server Components** - Add 'use client' only when needed526- **Colocate data fetching** - Fetch data where it's used527- **Use Suspense boundaries** - Enable streaming for slow data528- **Leverage parallel routes** - Independent loading states529- **Use Server Actions** - For mutations with progressive enhancement530531### Don'ts532533- **Don't pass serializable data** - Server → Client boundary limitations534- **Don't use hooks in Server Components** - No useState, useEffect535- **Don't fetch in Client Components** - Use Server Components or React Query536- **Don't over-nest layouts** - Each layout adds to the component tree537- **Don't ignore loading states** - Always provide loading.tsx or Suspense538