| name | nextjs-patterns |
| description | Advanced Next.js patterns and best practices. Covers Server Actions, Route Handlers, cookies handling, useSearchParams with Suspense, parallel routes, intercepting routes, streaming, and common anti-patterns to avoid. CRITICAL for server actions ('use server' directive), setting cookies from client components, form handling, and URL query parameters. Use when implementing mutations, API routes, complex routing patterns, or reviewing code for Next.js best practices. |
| allowed-tools | Read, Write, Edit, Glob, Grep, Bash |
Next.js Advanced Patterns
TypeScript: NEVER Use any Type
CRITICAL: This codebase has @typescript-eslint/no-explicit-any enabled.
async function handleSubmit(e: any) { ... }
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) { ... }
Part 1: Server Actions
Basic Server Action
'use server';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
if (!title) throw new Error('Title required');
await db.posts.create({ data: { title } });
revalidatePath('/posts');
}
Using in Forms
Pattern 1: Simple Form (no feedback needed)
import { createPost } from './actions';
export default function Page() {
return (
<form action={createPost}>
<input name="title" required />
<button type="submit">Create</button>
</form>
);
}
Pattern 2: With useActionState (for feedback)
'use client';
import { useActionState } from 'react';
import { createPost } from './actions';
export default function Page() {
const [state, action, isPending] = useActionState(createPost, null);
return (
<form action={action}>
<input name="title" required />
<button disabled={isPending}>
{isPending ? 'Creating...' : 'Create'}
</button>
{state?.error && <p className="error">{state.error}</p>}
</form>
);
}
'use server';
export async function createPost(prevState: unknown, formData: FormData) {
const title = formData.get('title') as string;
if (!title) return { error: 'Title required' };
await db.posts.create({ data: { title } });
return { success: true };
}
Client Component Calling Server Action
Two-file pattern (required):
'use server';
import { cookies } from 'next/headers';
export async function setTheme(theme: 'light' | 'dark') {
const cookieStore = await cookies();
cookieStore.set('theme', theme, {
httpOnly: true,
maxAge: 60 * 60 * 24 * 365,
});
}
'use client';
import { setTheme } from './actions';
export default function ThemeToggle() {
return (
<button onClick={() => setTheme('dark')}>
Dark Mode
</button>
);
}
Server Action with Redirect
'use server';
import { redirect } from 'next/navigation';
export async function login(formData: FormData) {
const email = formData.get('email') as string;
const session = await authenticate(email);
if (!session) throw new Error('Invalid credentials');
const cookieStore = await cookies();
cookieStore.set('session', session.token, { httpOnly: true });
redirect('/dashboard');
}
Part 2: Route Handlers (API Routes)
Basic Route Handler
export async function GET() {
const posts = await db.posts.findMany();
return Response.json(posts);
}
export async function POST(request: Request) {
const body = await request.json();
const post = await db.posts.create({ data: body });
return Response.json(post, { status: 201 });
}
Dynamic Route Handler
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const post = await db.posts.findUnique({ where: { id } });
if (!post) {
return Response.json({ error: 'Not found' }, { status: 404 });
}
return Response.json(post);
}
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
await db.posts.delete({ where: { id } });
return Response.json({ success: true });
}
Headers and Cookies in Route Handlers
import { cookies, headers } from 'next/headers';
export async function GET() {
const headersList = await headers();
const auth = headersList.get('authorization');
const cookieStore = await cookies();
const session = cookieStore.get('session');
if (!session) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
return Response.json({ user: await getUser(session.value) });
}
Streaming Response
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 10; i++) {
controller.enqueue(encoder.encode(`data: ${i}\n\n`));
await new Promise(r => setTimeout(r, 1000));
}
controller.close();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
},
});
}
Part 3: useSearchParams Pattern
CRITICAL: Requires 'use client' AND <Suspense> wrapper
'use client';
import { Suspense } from 'react';
import { useSearchParams, useRouter } from 'next/navigation';
function SearchContent() {
const searchParams = useSearchParams();
const router = useRouter();
const query = searchParams.get('q') || '';
const category = searchParams.get('category') || 'all';
const updateParams = (key: string, value: string) => {
const params = new URLSearchParams(searchParams.toString());
if (value) params.set(key, value);
else params.delete(key);
router.push(`?${params.toString()}`);
};
return (
<div>
<input
value={query}
onChange={(e) => updateParams('q', e.target.value)}
placeholder="Search..."
/>
<select
value={category}
onChange={(e) => updateParams('category', e.target.value)}
>
<option value="all">All</option>
<option value="electronics">Electronics</option>
</select>
<p>Results for: {query} in {category}</p>
</div>
);
}
export default function SearchPage() {
return (
<Suspense fallback={<div>Loading...</div>}>
<SearchContent />
</Suspense>
);
}
Server Component Alternative
export default async function SearchPage({
searchParams,
}: {
searchParams: Promise<{ q?: string; category?: string }>;
}) {
const { q, category } = await searchParams;
const results = await search(q, category);
return <ResultsList results={results} />;
}
Part 4: Cookies Pattern
Setting Cookies from Client Component
'use server';
import { cookies } from 'next/headers';
export async function setPreference(key: string, value: string) {
const cookieStore = await cookies();
cookieStore.set(key, value, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 365,
});
}
'use client';
import { setPreference } from './actions';
export default function PreferenceButton() {
return (
<button onClick={() => setPreference('theme', 'dark')}>
Enable Dark Mode
</button>
);
}
Reading Cookies
Server Components:
import { cookies } from 'next/headers';
export default async function Page() {
const cookieStore = await cookies();
const theme = cookieStore.get('theme')?.value || 'light';
return <div className={theme}>Content</div>;
}
Client Components (limited - non-httpOnly only):
'use client';
import { useEffect, useState } from 'react';
export default function ThemeDisplay() {
const [theme, setTheme] = useState('light');
useEffect(() => {
const match = document.cookie.match(/theme=([^;]+)/);
if (match) setTheme(match[1]);
}, []);
return <div>Theme: {theme}</div>;
}
Part 5: Parallel & Intercepting Routes
Parallel Routes
app/
├── dashboard/
│ ├── @analytics/
│ │ └── page.tsx
│ ├── @team/
│ │ └── page.tsx
│ ├── layout.tsx
│ └── page.tsx
export default function DashboardLayout({
children,
analytics,
team,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<div>
{children}
<div className="grid grid-cols-2">
{analytics}
{team}
</div>
</div>
);
}
Intercepting Routes (Modal Pattern)
app/
├── photos/
│ ├── [id]/
│ │ └── page.tsx # Full photo page
│ └── page.tsx # Photo gallery
├── @modal/
│ ├── (.)photos/
│ │ └── [id]/
│ │ └── page.tsx # Modal photo view
│ └── default.tsx # Return null
└── layout.tsx
export default function Layout({
children,
modal,
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
return (
<>
{children}
{modal}
</>
);
}
import Modal from '@/components/Modal';
export default async function PhotoModal({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const photo = await getPhoto(id);
return (
<Modal>
<img src={photo.url} alt={photo.title} />
</Modal>
);
}
export default function Default() {
return null;
}
Part 6: Anti-Patterns to Avoid
❌ Using useEffect for Data Fetching
'use client';
export default function Posts() {
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch('/api/posts').then(r => r.json()).then(setPosts);
}, []);
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
export default async function Posts() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
❌ Over-using 'use client'
'use client';
export default function Page() {
return (
<div>
<Header />
<StaticContent />
<InteractiveButton />
</div>
);
}
export default function Page() {
return (
<div>
<Header />
<StaticContent />
<InteractiveButton /> {/* Only this has 'use client' */}
</div>
);
}
❌ Serial Await (Waterfall)
const user = await fetchUser();
const posts = await fetchPosts();
const [user, posts] = await Promise.all([
fetchUser(),
fetchPosts(),
]);
❌ Using window.location for Navigation
window.location.href = '/dashboard';
import { useRouter } from 'next/navigation';
const router = useRouter();
router.push('/dashboard');
import Link from 'next/link';
<Link href="/dashboard">Go</Link>
❌ useState for Derived Values
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(products.reduce((sum, p) => sum + p.price, 0));
}, [products]);
const total = products.reduce((sum, p) => sum + p.price, 0);
const total = useMemo(
() => products.reduce((sum, p) => sum + p.price, 0),
[products]
);
❌ Missing Suspense for useSearchParams
'use client';
export default function Page() {
const searchParams = useSearchParams();
return <div>{searchParams.get('q')}</div>;
}
'use client';
function SearchContent() {
const searchParams = useSearchParams();
return <div>{searchParams.get('q')}</div>;
}
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<SearchContent />
</Suspense>
);
}
Quick Reference
Server Actions Checklist
Common Patterns
| Pattern | Files | Key Points |
|---|
| Server Action | actions.ts | 'use server', FormData, revalidate |
| Client → Server cookie | actions.ts + Component.tsx | Two files required |
| useSearchParams | Single file | 'use client' + <Suspense> |
| Route Handler | route.ts | HTTP methods, await params |
| Parallel Routes | @slot/page.tsx | Layout receives as props |
| Intercepting Routes | (.)route/page.tsx | Modal pattern |
Anti-Pattern Detection Checklist