--- name: nextjs-app-router-expert description: "Expert guide for Next.js 15 App Router: RSC, React 19 Server Actions, Async Request APIs, 'use cache', PPR, Streaming, and Caching strategies / Panduan ahli untuk Next.js 15 App Router." author: "Roedy Rustam" version: "4.4.2" --- # Next.js 15 App Router Expert (2026 Edition) [English](#english) | [Bahasa Indonesia](#bahasa-indonesia) --- ## English ### Orchestration & Integration - **Upstream Ideation & System Design**: `brainstorming`, `zero-to-prod-orchestrator` (Phase 3 & Phase 4). - **Frontend & Design System**: `senior-frontend`, `design-system-architect`, `tailwind-expert`, `modern-css-native-expert`. - **State & Data Fetching**: `tanstack-query-expert`, `form-validation-expert`, `database-orm-expert`. - **Quality & Performance**: `performance-web-vitals`, `e2e-testing-expert`, `anti-slop`, `visual-ui-auditor`. ### Description Production-grade architectural guide for **Next.js 15+ App Router** and **React 19**. Enforces modern standards: asynchronous request APIs (`await params`, `await cookies()`), function-level `'use cache'` with `cacheLife` and `cacheTag`, Partial Prerendering (PPR), React Server Actions with `useActionState` and `useOptimistic`, Parallel & Intercepting Routes, and streaming layouts via ``. ### Trigger Conditions - Architecting new Next.js 15+ applications or migrating legacy `pages/` directory code. - Handling asynchronous request parameters (`params`, `searchParams`, `cookies`, `headers`). - Implementing atomic mutations using React 19 Server Actions without manual REST endpoints. - Setting up granular caching with the new `'use cache'` directive and cache invalidation tags. - Building complex modal navigation patterns with Parallel and Intercepting Routes. - Optimizing Core Web Vitals (LCP, INP, TTFB) with Partial Prerendering and Streaming. --- ### Core Architectural Patterns #### 1. Async Request APIs (Mandatory in Next.js 15) In Next.js 15, dynamic APIs (`params`, `searchParams`, `cookies()`, `headers()`) are asynchronous Promises to enable future runtime streaming and static preloading: ```typescript // app/blog/[slug]/page.tsx import { cookies, headers } from 'next/headers'; import { notFound } from 'next/navigation'; import { db } from '@/lib/db'; interface PageProps { params: Promise<{ slug: string }>; searchParams: Promise<{ preview?: string }>; } export default async function BlogPostPage({ params, searchParams }: PageProps) { // Await params and searchParams concurrently const [{ slug }, { preview }] = await Promise.all([params, searchParams]); const cookieStore = await cookies(); const headerStore = await headers(); const isPreview = preview === 'true' && cookieStore.get('preview_token')?.value === process.env.PREVIEW_SECRET; const post = await db.posts.findUnique({ where: { slug } }); if (!post || (!post.published && !isPreview)) { notFound(); } return (

{post.title}

User-Agent: {headerStore.get('user-agent')}

{post.content}
); } ``` #### 2. Next-Gen Caching with `'use cache'`, `cacheLife()`, and `cacheTag()` Next.js 15 replaces opaque fetch options with explicit, declarative function and component-level caching: ```typescript // lib/cached-data.ts import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'; import { db } from '@/lib/db'; export async function getProductCatalog(category: string) { 'use cache'; cacheLife('hours'); // Predefined profile: 'seconds', 'minutes', 'hours', 'days', 'weeks', 'max' cacheTag('products', `category-${category}`); return await db.products.findMany({ where: { category, inStock: true }, select: { id: true, name: true, price: true, category: true }, orderBy: { createdAt: 'desc' }, }); } ``` ```typescript // In Server Action (app/actions/products.ts) 'use server'; import { revalidateTag } from 'next/cache'; import { db } from '@/lib/db'; export async function updateProductStock(productId: string, inStock: boolean) { await db.products.update({ where: { id: productId }, data: { inStock } }); revalidateTag('products'); // Purges all cached product queries instantly return { success: true }; } ``` #### 3. React 19 Server Actions with `useActionState` & `useOptimistic` Atomic form mutations with automatic pending states, error validation, and zero-latency optimistic updates: ```tsx // app/tasks/task-form.tsx 'use client'; import { useActionState, useOptimistic, startTransition } from 'react'; import { createTaskAction, type TaskState } from '@/app/actions/task'; interface Task { id: string; title: string; pending?: boolean; } const initialState: TaskState = { success: false, error: null }; export function TaskManager({ initialTasks }: { initialTasks: Task[] }) { const [state, formAction, isPending] = useActionState(createTaskAction, initialState); const [optimisticTasks, addOptimisticTask] = useOptimistic( initialTasks, (currentTasks, newTitle: string) => [ ...currentTasks, { id: `temp-${Date.now()}`, title: newTitle, pending: true }, ] ); const handleClientSubmit = (formData: FormData) => { const title = formData.get('title') as string; if (!title?.trim()) return; startTransition(async () => { addOptimisticTask(title); await formAction(formData); }); }; return (
{state.error &&

{state.error}

}
); } ``` #### 4. Parallel & Intercepting Routes (URL-Preserving Modals) Enable modal dialogues with dedicated shareable URLs and seamless backdrop preservation: ``` app/ ├── @modal/ │ ├── (.)photos/[id]/page.tsx <-- Intercepts client navigation to /photos/[id] │ └── default.tsx <-- Returns null when modal is inactive ├── photos/ │ └── [id]/page.tsx <-- Renders standalone full page on direct refresh ├── layout.tsx <-- Renders children AND @modal slot └── page.tsx ``` ```tsx // app/@modal/(.)photos/[id]/page.tsx import { Modal } from '@/components/ui/modal'; import { PhotoView } from '@/components/photo-view'; export default async function PhotoModal({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; return ( ); } ``` #### 5. Partial Prerendering (PPR) & Streaming Suspense Combines instantaneous static edge delivery with dynamic personalized punch-outs: ```tsx // app/dashboard/page.tsx import { Suspense } from 'react'; import { Skeleton } from '@/components/ui/skeleton'; export const experimental_ppr = true; // Enables PPR for this route segment export default function DashboardPage() { return (
{/* Static Shell: Prerendered at build time and served immediately from CDN */}

Analytics Dashboard

Real-time telemetry and revenue tracking

{/* Dynamic Punch-out: Streamed dynamically from the server */} }>
); } async function AsyncMetricsStream() { const metrics = await fetchLiveMetrics(); return (
{metrics.map((m) => (
{m.name}
{m.value}
))}
); } async function fetchLiveMetrics() { return [ { name: 'Active Sessions', value: '4,281' }, { name: 'Average Latency', value: '18ms' }, { name: 'Success Rate', value: '99.98%' }, ]; } ``` ### Sovereign Execution, Token Efficiency & Invariants | Dimension | Sovereign Mandate & Deterministic Contract | | :--- | :--- | | **Frontier Model Cascade** | Medium / High (`Gemini 4 Argon`, `Claude Sonnet 5.5 High`, `Muse Wonder High`, `Composer 2.5 Fast`) | | **Availability Fallback** | `Gemini 4 Argon/Pro` -> `Gemini 3.8 Flash`; `Claude Opus 5.5` -> `Claude Opus 4.6` (Zero unhandled missing-model errors) | | **KV-Cache & Token Protocol** | Pin static instructions & schemas at prompt prefix (`kv-cache-prefix-optimizer`); pass surgical `CONTEXT_MAP.md` slices (<250 tokens/call) during Swarm Fan-Out | | **SaaT CLI & MCP Dispatch** | CLI: `vibes skill exec nextjs-app-router-expert ` / MCP: `vibes_skill_dispatch`, `vibes_visual_audit`, `vibes_ast_query`, `vibes_verify_gate` | | **NFR & Anti-Slop Invariants** | LCP < 1.8s, INP < 100ms, CLS < 0.05, 1px crisp borders, 3-tier radius (6px/8px/12px), `tabular-nums`, 5-state interactive contract; 100% complete zero-placeholder output verified via `node scripts/check-anti-slop.js --strict` | --- --- ## Bahasa Indonesia ### Integrasi Orkestrasi - **Ideasi Hulu & Desain Sistem**: `brainstorming`, `zero-to-prod-orchestrator` (Fase 3 & Fase 4). - **Frontend & Design System**: `senior-frontend`, `design-system-architect`, `tailwind-expert`, `modern-css-native-expert`. - **State & Pengambilan Data**: `tanstack-query-expert`, `form-validation-expert`, `database-orm-expert`. - **Kualitas & Performa**: `performance-web-vitals`, `e2e-testing-expert`, `anti-slop`, `visual-ui-auditor`. ### Deskripsi Panduan arsitektur tingkat produksi untuk **Next.js 15+ App Router** dan **React 19**. Menegakkan standar modern: API request asinkron (`await params`, `await cookies()`), caching tingkat fungsi `'use cache'` dengan `cacheLife` dan `cacheTag`, Partial Prerendering (PPR), React Server Actions dengan `useActionState` dan `useOptimistic`, Parallel & Intercepting Routes, serta streaming layout via ``. ### Kondisi Pemicu - Merancang aplikasi Next.js 15+ baru atau migrasi kode dari direktori `pages/`. - Menangani parameter request asinkron (`params`, `searchParams`, `cookies`, `headers`). - Mengimplementasikan mutasi data atomik menggunakan Server Actions React 19 tanpa endpoint REST manual. - Mengonfigurasi strategi cache terperinci dengan direktif `'use cache'` dan tag invalidasi. - Membangun antarmuka modal URL-preserving dengan Parallel dan Intercepting Routes. - Mengoptimalkan Core Web Vitals (LCP, INP, TTFB) melalui Partial Prerendering dan Streaming. --- ### Implementasi Pola Kunci #### 1. API Request Asinkron (Wajib di Next.js 15) Di Next.js 15, API dinamis (`params`, `searchParams`, `cookies()`, `headers()`) berbentuk Promise asinkron untuk mendukung streaming runtime dan optimasi preloading: ```typescript // app/blog/[slug]/page.tsx import { cookies, headers } from 'next/headers'; import { notFound } from 'next/navigation'; import { db } from '@/lib/db'; interface PageProps { params: Promise<{ slug: string }>; searchParams: Promise<{ preview?: string }>; } export default async function HalamanBlogPost({ params, searchParams }: PageProps) { // Await params dan searchParams secara bersamaan const [{ slug }, { preview }] = await Promise.all([params, searchParams]); const cookieStore = await cookies(); const headerStore = await headers(); const isPreview = preview === 'true' && cookieStore.get('preview_token')?.value === process.env.PREVIEW_SECRET; const post = await db.posts.findUnique({ where: { slug } }); if (!post || (!post.published && !isPreview)) { notFound(); } return (

{post.title}

User-Agent: {headerStore.get('user-agent')}

{post.content}
); } ``` #### 2. Caching Generasi Baru dengan `'use cache'`, `cacheLife()`, dan `cacheTag()` Next.js 15 menggantikan opsi fetch manual dengan caching deklaratif tingkat fungsi dan komponen: ```typescript // lib/cached-data.ts import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'; import { db } from '@/lib/db'; export async function ambilKatalogProduk(kategori: string) { 'use cache'; cacheLife('hours'); // Profil cache bawaan: 'seconds', 'minutes', 'hours', 'days', 'weeks', 'max' cacheTag('produk', `kategori-${kategori}`); return await db.products.findMany({ where: { category: kategori, inStock: true }, select: { id: true, name: true, price: true, category: true }, orderBy: { createdAt: 'desc' }, }); } ``` ```typescript // Dalam Server Action (app/actions/products.ts) 'use server'; import { revalidateTag } from 'next/cache'; import { db } from '@/lib/db'; export async function perbaruiStokProduk(productId: string, inStock: boolean) { await db.products.update({ where: { id: productId }, data: { inStock } }); revalidateTag('produk'); // Menghapus seluruh cache query produk seketika return { success: true }; } ``` #### 3. React 19 Server Actions dengan `useActionState` & `useOptimistic` Mutasi formulir atomik dengan penanganan loading otomatis, validasi error server, dan update UI optimistik instan: ```tsx // app/tasks/task-form.tsx 'use client'; import { useActionState, useOptimistic, startTransition } from 'react'; import { buatTugasAction, type StatusTugas } from '@/app/actions/task'; interface Tugas { id: string; title: string; pending?: boolean; } const statusAwal: StatusTugas = { success: false, error: null }; export function PengelolaTugas({ tugasAwal }: { tugasAwal: Tugas[] }) { const [state, formAction, isPending] = useActionState(buatTugasAction, statusAwal); const [tugasOptimistik, tambahTugasOptimistik] = useOptimistic( tugasAwal, (daftarSekarang, judulBaru: string) => [ ...daftarSekarang, { id: `temp-${Date.now()}`, title: judulBaru, pending: true }, ] ); const tanganiSubmitKlien = (formData: FormData) => { const title = formData.get('title') as string; if (!title?.trim()) return; startTransition(async () => { tambahTugasOptimistik(title); await formAction(formData); }); }; return (
{state.error &&

{state.error}

}
    {tugasOptimistik.map((tugas) => (
  • {tugas.title} {tugas.pending && Menyinkronkan...}
  • ))}
); } ``` #### 4. Parallel & Intercepting Routes (Modal Preservasi URL) Menghadirkan dialog modal dengan URL mandiri yang dapat dibagikan tanpa menghilangkan state halaman latar: ``` app/ ├── @modal/ │ ├── (.)photos/[id]/page.tsx <-- Mencegat navigasi klien ke /photos/[id] │ └── default.tsx <-- Mengembalikan null saat modal tidak aktif ├── photos/ │ └── [id]/page.tsx <-- Memuat halaman utuh saat di-refresh langsung ├── layout.tsx <-- Me-render children DAN slot @modal └── page.tsx ``` #### 5. Partial Prerendering (PPR) & Streaming Suspense Menggabungkan kecepatan delivery shell statis dari CDN edge dengan data dinamis real-time: ```tsx // app/dashboard/page.tsx import { Suspense } from 'react'; import { Skeleton } from '@/components/ui/skeleton'; export const experimental_ppr = true; // Mengaktifkan PPR pada rute ini export default function HalamanDashboard() { return (
{/* Shell Statis: Di-prerender saat build time dan disajikan instan dari CDN */}

Dashboard Analitik

Pelacakan telemetri dan pendapatan real-time

{/* Bagian Dinamis: Di-stream secara bertahap dari server */} }>
); } async function StreamMetrikLangsung() { const metrik = await ambilMetrikSistem(); return (
{metrik.map((m) => (
{m.nama}
{m.nilai}
))}
); } async function ambilMetrikSistem() { return [ { nama: 'Sesi Aktif', nilai: '4,281' }, { nama: 'Rata-rata Latensi', nilai: '18ms' }, { nama: 'Tingkat Keberhasilan', nilai: '99.98%' }, ]; } ``` ### Matriks Eksekusi Berdaulat, Efisiensi Token & Invarian | Dimensi | Mandat Berdaulat & Kontrak Deterministik | | :--- | :--- | | **Kaskade Model Frontier** | Medium / High (`Gemini 4 Argon`, `Claude Sonnet 5.5 High`, `Muse Wonder High`, `Composer 2.5 Fast`) | | **Invarian Fallback Ketersediaan** | `Gemini 4 Argon/Pro` -> `Gemini 3.8 Flash`; `Claude Opus 5.5` -> `Claude Opus 4.6` (Bebas gagal saat model belum tersedia/kuota penuh) | | **Protokol Hemat Token & KV-Cache** | Kunci instruksi & skema statis di prefix awal (`kv-cache-prefix-optimizer`); kirim potongan `CONTEXT_MAP.md` (<250 token/panggilan) saat Fan-Out | | **Eksekusi SaaT CLI & MCP** | CLI: `vibes skill exec nextjs-app-router-expert ` / MCP: `vibes_skill_dispatch`, `vibes_visual_audit`, `vibes_ast_query`, `vibes_verify_gate` | | **Invarian NFR & Anti-Slop** | LCP < 1.8s, INP < 100ms, CLS < 0.05, border tegas 1px, radius 3-tier (6px/8px/12px), `tabular-nums`, kontrak interaktif 5-state; kode 100% tuntas tanpa placeholder diverifikasi melalui `node scripts/check-anti-slop.js --strict` |