Agent skill
nextjs-app-router-patterns
Next.js App Router patterns — server vs client components, data fetching, caching, streaming, parallel routes, and error boundaries
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/nextjs-app-router-patterns-generaljerel-chalk-skills
SKILL.md
Next.js App Router Patterns
Overview
Reference guide for idiomatic Next.js 14+ App Router patterns. Apply these when building pages, layouts, and data flows to fully leverage server components, streaming, and the caching system.
Server vs Client Component Decision Tree
Does the component need...
├── Browser APIs (window, localStorage, navigator)?
│ └── YES → "use client"
├── Event listeners (onClick, onChange, onSubmit)?
│ └── YES → "use client"
├── useState, useEffect, useRef, or custom hooks?
│ └── YES → "use client"
├── Only data fetching and rendering?
│ └── YES → Server Component (default) ✓
├── Third-party library that uses hooks or browser APIs?
│ └── YES → "use client" wrapper component
└── Static content (no interactivity)?
└── YES → Server Component (default) ✓
Key Principle: Push "use client" Down
Keep the boundary as low as possible. Only the interactive leaf should be a client component.
app/
dashboard/
page.tsx ← Server Component (fetches data)
DashboardGrid.tsx ← Server Component (layout)
MetricCard.tsx ← Server Component (displays data)
ChartWidget.tsx ← "use client" (uses chart library with hooks)
FilterBar.tsx ← "use client" (has onChange handlers)
// app/dashboard/page.tsx — Server Component
import { getMetrics } from "@/lib/data";
import { DashboardGrid } from "./DashboardGrid";
import { FilterBar } from "./FilterBar";
export default async function DashboardPage() {
const metrics = await getMetrics(); // Direct DB/API call, no useEffect
return (
<div>
<FilterBar /> {/* Client island for interactivity */}
<DashboardGrid metrics={metrics} /> {/* Server component */}
</div>
);
}
Data Fetching Patterns
Server Components: Direct Async
// app/users/page.tsx
import { db } from "@/lib/db";
export default async function UsersPage() {
const users = await db.user.findMany({
orderBy: { createdAt: "desc" },
});
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Parallel Data Fetching
Fetch multiple resources in parallel with Promise.all to avoid waterfalls.
export default async function ProfilePage({ params }: { params: { id: string } }) {
// Parallel — both start immediately
const [user, posts] = await Promise.all([
getUser(params.id),
getUserPosts(params.id),
]);
return (
<>
<UserHeader user={user} />
<PostList posts={posts} />
</>
);
}
Data Fetching with Preloading
For components deeper in the tree, use the preload pattern.
// lib/data.ts
import { cache } from "react";
export const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});
// Preload function — call early, consume later
export const preloadUser = (id: string) => {
void getUser(id);
};
// app/user/[id]/page.tsx
import { getUser, preloadUser } from "@/lib/data";
export default async function UserPage({ params }: { params: { id: string } }) {
preloadUser(params.id); // Start fetch early
// ... other work ...
const user = await getUser(params.id); // Deduplicated, may already be resolved
return <Profile user={user} />;
}
Caching Strategies
Fetch Cache (Request Deduplication)
// These two calls in the same render are automatically deduplicated
async function UserName({ id }: { id: string }) {
const user = await fetch(`/api/users/${id}`); // cached
return <span>{user.name}</span>;
}
async function UserAvatar({ id }: { id: string }) {
const user = await fetch(`/api/users/${id}`); // same URL = deduplicated
return <img src={user.avatar} />;
}
Revalidation
// Time-based revalidation
const data = await fetch(url, { next: { revalidate: 60 } }); // Revalidate every 60s
// On-demand revalidation (in a Server Action or Route Handler)
import { revalidatePath, revalidateTag } from "next/cache";
// By path
revalidatePath("/dashboard");
// By tag
const data = await fetch(url, { next: { tags: ["users"] } });
// Later:
revalidateTag("users");
// Opt out of caching
const data = await fetch(url, { cache: "no-store" });
Route Segment Config
// app/dashboard/layout.tsx
export const dynamic = "force-dynamic"; // Never cache this segment
export const revalidate = 60; // Revalidate every 60s
Streaming with Suspense
Wrap slow parts in Suspense to stream the page progressively.
// app/dashboard/page.tsx
import { Suspense } from "react";
import { SlowAnalytics } from "./SlowAnalytics";
import { QuickSummary } from "./QuickSummary";
export default function DashboardPage() {
return (
<div>
{/* Renders immediately */}
<QuickSummary />
{/* Streams in when ready */}
<Suspense fallback={<AnalyticsSkeleton />}>
<SlowAnalytics />
</Suspense>
</div>
);
}
Loading States (file convention)
app/
dashboard/
page.tsx
loading.tsx ← Automatic Suspense boundary for the page
error.tsx ← Error boundary for the segment
not-found.tsx ← Shown when notFound() is called
// app/dashboard/loading.tsx
export default function Loading() {
return <DashboardSkeleton />;
}
Parallel Routes
Render multiple pages in the same layout simultaneously.
app/
@analytics/
page.tsx ← Analytics panel
loading.tsx
@feed/
page.tsx ← Feed panel
loading.tsx
layout.tsx ← Receives both as props
// app/layout.tsx
export default function Layout({
children,
analytics,
feed,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
feed: React.ReactNode;
}) {
return (
<div className="grid grid-cols-3">
<main className="col-span-2">{children}</main>
<aside>
{analytics}
{feed}
</aside>
</div>
);
}
Intercepting Routes
Show a route in a modal while preserving the background page.
app/
photos/
[id]/
page.tsx ← Full photo page (direct navigation)
@modal/
(.)photos/[id]/
page.tsx ← Modal overlay (intercepted from feed)
default.tsx ← Returns null when no modal active
layout.tsx
page.tsx ← Photo feed
// app/@modal/(.)photos/[id]/page.tsx
import { Modal } from "@/components/Modal";
import { getPhoto } from "@/lib/data";
export default async function PhotoModal({ params }: { params: { id: string } }) {
const photo = await getPhoto(params.id);
return (
<Modal>
<img src={photo.url} alt={photo.alt} />
</Modal>
);
}
Error Boundaries
// app/dashboard/error.tsx
"use client"; // Error boundaries must be client components
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div role="alert">
<h2>Something went wrong</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}
Anti-patterns
"use client" on Everything
Adding "use client" to every component defeats the purpose of RSC. You lose direct data access, increase bundle size, and add unnecessary hydration.
Client-side Data Fetching When Server Would Work
// BAD: useEffect + fetch in a page component
"use client";
export default function UsersPage() {
const [users, setUsers] = useState([]);
useEffect(() => { fetch("/api/users").then(/* ... */) }, []);
}
// GOOD: Server component with direct data access
export default async function UsersPage() {
const users = await db.user.findMany();
return <UserList users={users} />;
}
Misunderstanding Caching
fetchin server components is cached by default in productionPOSTrequests are NOT cached- Route Handlers with
GETare cached when usingexport const dynamic = "auto" - Dynamic functions (
cookies(),headers(),searchParams) opt the entire route out of static rendering
Not Leveraging Layouts
Layouts persist across navigations and do not re-render. Put shared UI (nav, sidebar) in layouts, not in every page.
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?