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.
references/details.md
1# nextjs-app-router-patterns — detailed patterns and worked examples23## Patterns45### Pattern 1: Server Components with Data Fetching67```typescript8// app/products/page.tsx9import { Suspense } from 'react'10import { ProductList, ProductListSkeleton } from '@/components/products'11import { FilterSidebar } from '@/components/filters'1213interface SearchParams {14category?: string15sort?: 'price' | 'name' | 'date'16page?: string17}1819export default async function ProductsPage({20searchParams,21}: {22searchParams: Promise<SearchParams>23}) {24const params = await searchParams2526return (27<div className="flex gap-8">28<FilterSidebar />29<Suspense30key={JSON.stringify(params)}31fallback={<ProductListSkeleton />}32>33<ProductList34category={params.category}35sort={params.sort}36page={Number(params.page) || 1}37/>38</Suspense>39</div>40)41}4243// components/products/ProductList.tsx - Server Component44async function getProducts(filters: ProductFilters) {45const res = await fetch(46`${process.env.API_URL}/products?${new URLSearchParams(filters)}`,47{ next: { tags: ['products'] } }48)49if (!res.ok) throw new Error('Failed to fetch products')50return res.json()51}5253export async function ProductList({ category, sort, page }: ProductFilters) {54const { products, totalPages } = await getProducts({ category, sort, page })5556return (57<div>58<div className="grid grid-cols-3 gap-4">59{products.map((product) => (60<ProductCard key={product.id} product={product} />61))}62</div>63<Pagination currentPage={page} totalPages={totalPages} />64</div>65)66}67```6869### Pattern 2: Client Components with 'use client'7071```typescript72// components/products/AddToCartButton.tsx73'use client'7475import { useState, useTransition } from 'react'76import { addToCart } from '@/app/actions/cart'7778export function AddToCartButton({ productId }: { productId: string }) {79const [isPending, startTransition] = useTransition()80const [error, setError] = useState<string | null>(null)8182const handleClick = () => {83setError(null)84startTransition(async () => {85const result = await addToCart(productId)86if (result.error) {87setError(result.error)88}89})90}9192return (93<div>94<button95onClick={handleClick}96disabled={isPending}97className="btn-primary"98>99{isPending ? 'Adding...' : 'Add to Cart'}100</button>101{error && <p className="text-red-500 text-sm">{error}</p>}102</div>103)104}105```106107### Pattern 3: Server Actions108109```typescript110// app/actions/cart.ts111"use server";112113import { revalidateTag } from "next/cache";114import { cookies } from "next/headers";115import { redirect } from "next/navigation";116117export async function addToCart(productId: string) {118const cookieStore = await cookies();119const sessionId = cookieStore.get("session")?.value;120121if (!sessionId) {122redirect("/login");123}124125try {126await db.cart.upsert({127where: { sessionId_productId: { sessionId, productId } },128update: { quantity: { increment: 1 } },129create: { sessionId, productId, quantity: 1 },130});131132revalidateTag("cart");133return { success: true };134} catch (error) {135return { error: "Failed to add item to cart" };136}137}138139export async function checkout(formData: FormData) {140const address = formData.get("address") as string;141const payment = formData.get("payment") as string;142143// Validate144if (!address || !payment) {145return { error: "Missing required fields" };146}147148// Process order149const order = await processOrder({ address, payment });150151// Redirect to confirmation152redirect(`/orders/${order.id}/confirmation`);153}154```155156### Pattern 4: Parallel Routes157158```typescript159// app/dashboard/layout.tsx160export default function DashboardLayout({161children,162analytics,163team,164}: {165children: React.ReactNode166analytics: React.ReactNode167team: React.ReactNode168}) {169return (170<div className="dashboard-grid">171<main>{children}</main>172<aside className="analytics-panel">{analytics}</aside>173<aside className="team-panel">{team}</aside>174</div>175)176}177178// app/dashboard/@analytics/page.tsx179export default async function AnalyticsSlot() {180const stats = await getAnalytics()181return <AnalyticsChart data={stats} />182}183184// app/dashboard/@analytics/loading.tsx185export default function AnalyticsLoading() {186return <ChartSkeleton />187}188189// app/dashboard/@team/page.tsx190export default async function TeamSlot() {191const members = await getTeamMembers()192return <TeamList members={members} />193}194```195196### Pattern 5: Intercepting Routes (Modal Pattern)197198```typescript199// File structure for photo modal200// app/201// ├── @modal/202// │ ├── (.)photos/[id]/page.tsx # Intercept203// │ └── default.tsx204// ├── photos/205// │ └── [id]/page.tsx # Full page206// └── layout.tsx207208// app/@modal/(.)photos/[id]/page.tsx209import { Modal } from '@/components/Modal'210import { PhotoDetail } from '@/components/PhotoDetail'211212export default async function PhotoModal({213params,214}: {215params: Promise<{ id: string }>216}) {217const { id } = await params218const photo = await getPhoto(id)219220return (221<Modal>222<PhotoDetail photo={photo} />223</Modal>224)225}226227// app/photos/[id]/page.tsx - Full page version228export default async function PhotoPage({229params,230}: {231params: Promise<{ id: string }>232}) {233const { id } = await params234const photo = await getPhoto(id)235236return (237<div className="photo-page">238<PhotoDetail photo={photo} />239<RelatedPhotos photoId={id} />240</div>241)242}243244// app/layout.tsx245export default function RootLayout({246children,247modal,248}: {249children: React.ReactNode250modal: React.ReactNode251}) {252return (253<html>254<body>255{children}256{modal}257</body>258</html>259)260}261```262263### Pattern 6: Streaming with Suspense264265```typescript266// app/product/[id]/page.tsx267import { Suspense } from 'react'268269export default async function ProductPage({270params,271}: {272params: Promise<{ id: string }>273}) {274const { id } = await params275276// This data loads first (blocking)277const product = await getProduct(id)278279return (280<div>281{/* Immediate render */}282<ProductHeader product={product} />283284{/* Stream in reviews */}285<Suspense fallback={<ReviewsSkeleton />}>286<Reviews productId={id} />287</Suspense>288289{/* Stream in recommendations */}290<Suspense fallback={<RecommendationsSkeleton />}>291<Recommendations productId={id} />292</Suspense>293</div>294)295}296297// These components fetch their own data298async function Reviews({ productId }: { productId: string }) {299const reviews = await getReviews(productId) // Slow API300return <ReviewList reviews={reviews} />301}302303async function Recommendations({ productId }: { productId: string }) {304const products = await getRecommendations(productId) // ML-based, slow305return <ProductCarousel products={products} />306}307```308309### Pattern 7: Route Handlers (API Routes)310311```typescript312// app/api/products/route.ts313import { NextRequest, NextResponse } from "next/server";314315export async function GET(request: NextRequest) {316const searchParams = request.nextUrl.searchParams;317const category = searchParams.get("category");318319const products = await db.product.findMany({320where: category ? { category } : undefined,321take: 20,322});323324return NextResponse.json(products);325}326327export async function POST(request: NextRequest) {328const body = await request.json();329330const product = await db.product.create({331data: body,332});333334return NextResponse.json(product, { status: 201 });335}336337// app/api/products/[id]/route.ts338export async function GET(339request: NextRequest,340{ params }: { params: Promise<{ id: string }> },341) {342const { id } = await params;343const product = await db.product.findUnique({ where: { id } });344345if (!product) {346return NextResponse.json({ error: "Product not found" }, { status: 404 });347}348349return NextResponse.json(product);350}351```352353### Pattern 8: Metadata and SEO354355```typescript356// app/products/[slug]/page.tsx357import { Metadata } from 'next'358import { notFound } from 'next/navigation'359360type Props = {361params: Promise<{ slug: string }>362}363364export async function generateMetadata({ params }: Props): Promise<Metadata> {365const { slug } = await params366const product = await getProduct(slug)367368if (!product) return {}369370return {371title: product.name,372description: product.description,373openGraph: {374title: product.name,375description: product.description,376images: [{ url: product.image, width: 1200, height: 630 }],377},378twitter: {379card: 'summary_large_image',380title: product.name,381description: product.description,382images: [product.image],383},384}385}386387export async function generateStaticParams() {388const products = await db.product.findMany({ select: { slug: true } })389return products.map((p) => ({ slug: p.slug }))390}391392export default async function ProductPage({ params }: Props) {393const { slug } = await params394const product = await getProduct(slug)395396if (!product) notFound()397398return <ProductDetail product={product} />399}400```401402## Caching Strategies403404### Data Cache405406```typescript407// No cache (always fresh)408fetch(url, { cache: "no-store" });409410// Cache forever (static)411fetch(url, { cache: "force-cache" });412413// ISR - revalidate after 60 seconds414fetch(url, { next: { revalidate: 60 } });415416// Tag-based invalidation417fetch(url, { next: { tags: ["products"] } });418419// Invalidate via Server Action420("use server");421import { revalidateTag, revalidatePath } from "next/cache";422423export async function updateProduct(id: string, data: ProductData) {424await db.product.update({ where: { id }, data });425revalidateTag("products");426revalidatePath("/products");427}428```429