# Next.js

> ContextOS skill for Next.js

- Skill: `kok-o/next-js` (Agent Skill)
- Install (CLI): `npx skillmds@latest add kok-o/next-js`
- Raw SKILL.md: https://api.skillmd.com/api/skills/kok-o/next-js/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: kok-o (https://skillmd.com/u/kok-o)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/kok-o/next-js

---

# Next.js App Router Best Practices

## Overview

Enforces high-performance architectural patterns for Next.js App Router based on Vercel Engineering guidelines: React Server Components (RSC), zero-waterfall async pipelines, request deduplication via `React.cache()`, bundle optimization, and secure Server Actions.

## When to Use

Activate whenever building, refactoring, or reviewing Next.js pages, layouts, Route Handlers (`app/api`), Server Actions, or components in the `app/` directory.

## Negative Constraints (What NOT to Do)

1. **NEVER use barrel imports for UI libraries**: Avoid `import { Button, Dialog } from '@/components'`. Import directly from the exact file (`import { Button } from '@/components/ui/button'`) to prevent bundler tree-shaking failures and trace bloat.
2. **NEVER trust client-provided data or session state in Server Actions**: Always authenticate session and authorize tenant ownership inside the Server Action handler itself before mutating data.
3. **NEVER introduce sequential `await` waterfalls for independent data**: Always use `Promise.all()` or parallel streaming `<Suspense>` boundaries.
4. **NEVER pass large unneeded serialized data from Server to Client Components**: Only pass the specific primitive fields required by the client component (`server-dedup-props`).
5. **NEVER use `useEffect` for data fetching**: Fetch directly in Server Components or use TanStack Query / SWR for client-side queries.
6. **NEVER import server-only modules in client components**: Use the `server-only` package in data access layers to catch accidental client imports at build time.

## Rules & Patterns

### 1. Eliminating Async Waterfalls (Critical)

- **Parallel Fetching**: Fetch independent data concurrently at the top of the route or component.
- **Granular Streaming**: Wrap slow, non-critical subtrees in `<Suspense fallback={<Skeleton />}>` so critical above-the-fold content streams immediately.
- **Defer Awaits**: Check cheap synchronous conditions before awaiting remote resources.

### 2. Request Deduplication & Caching (`server-cache-react`)

- Use `React.cache()` to deduplicate identical database or service calls across multiple components rendered in the same server request lifecycle.

```tsx
import { cache } from 'react';
import { db } from '@/lib/db';

export const getCurrentUser = cache(async (userId: string) => {
  return await db.user.findUnique({
    where: { id: userId },
    select: { id: true, name: true, role: true, email: true }
  });
});
```

### 3. Secure Server Actions (`server-auth-actions`)

- Treat every Server Action as a public HTTP endpoint. Always validate session, authorization, and input schema with Zod.

```tsx
'use server';

import { z } from 'zod';
import { auth } from '@/lib/auth';
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';

const UpdateProfileSchema = z.object({
  name: z.string().min(2).max(50),
});

export async function updateProfile(formData: FormData) {
  const session = await auth();
  if (!session?.userId) throw new Error('Unauthorized');

  const result = UpdateProfileSchema.safeParse({ name: formData.get('name') });
  if (!result.success) return { error: 'Invalid input', issues: result.error.flatten() };

  await db.user.update({
    where: { id: session.userId },
    data: { name: result.data.name },
  });

  revalidatePath('/settings');
  return { success: true };
}
```

### 4. Bundle Optimization & Dynamic Imports (`bundle-dynamic-imports`)

- Heavy interactive client components (charts, rich-text editors, video players) must be dynamically loaded with `next/dynamic`.

```tsx
import dynamic from 'next/dynamic';

const AnalyticsChart = dynamic(
  () => import('@/components/analytics/chart').then(mod => mod.AnalyticsChart),
  {
    loading: () => <div className="h-64 animate-pulse bg-muted rounded-lg" />,
    ssr: false,
  }
);
```

### 5. Next.js 15+ Async Request APIs (`async-params`)

In Next.js 15+, `params`, `searchParams`, `cookies()`, and `headers()` are asynchronous and must be awaited:

```tsx
// [GOOD] Next.js 15+ Page Component
interface PageProps {
  params: Promise<{ id: string }>;
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}

export default async function UserPage({ params, searchParams }: PageProps) {
  const { id } = await params;
  const { tab } = await searchParams;
  const user = await getUser(id);

  return <UserProfile user={user} activeTab={tab as string} />;
}
```

### 6. Non-Blocking Background Tasks with `after()`

To execute logging, analytics, or cache priming without delaying the user's HTTP response:

```typescript
import { after } from 'next/server';

export async function POST(request: Request) {
  const data = await request.json();
  const result = await processOrder(data);

  // Executes asynchronously AFTER the response stream has completed
  after(async () => {
    await sendSlackNotification(result);
    await indexOrderInSearch(result.id);
  });

  return Response.json({ success: true, orderId: result.id });
}
```

---

## Code Examples

See `EXAMPLES.md` for detailed code examples and component templates.

## Validation Checklist

- [ ] All database queries in RSC layers use `React.cache()` if called across multiple components.
- [ ] No barrel imports (`from '@/components'`); all imports point to exact component modules.
- [ ] Server Actions have explicit auth checks and Zod input validation.
- [ ] Heavy client widgets (charts, editors) use `next/dynamic`.
- [ ] Images use `next/image` with explicit `sizes` and `priority` on LCP elements.

## Common Mistakes

- Using `'use client'` at page level instead of leaf components.
- Relying on client-side authentication checks for Server Actions without server-side validation.
- Chaining sequential awaits for independent data models.

## Integration Notes

- Pairs with `react` and `ui-ux-pro` for component design and state management.
- Pairs with `security` for session authorization and input sanitization.


<!-- Source: nextjs.md -->

# Next.js — Best Practices (App Router)

## Routing

- Use **App Router** (`app/` directory) — not Pages Router
- **Layouts** — shared UI in `layout.tsx`, nested layouts for sections
- **Loading states** — `loading.tsx` for Suspense boundaries
- **Error handling** — `error.tsx` for error boundaries per route
- **Not found** — `not-found.tsx` for 404 pages

## Server vs Client Components

- **Default to Server Components** — they're server by default
- **Use `'use client'`** only when you need: event handlers, useState, useEffect, browser APIs
- **Push client boundaries down** — keep as much as possible on the server
- **Don't pass functions** from Server to Client components

## Data Fetching

- **Server Components** — fetch directly, no useEffect
- **Server Actions** — for mutations (`'use server'`)
- **Route Handlers** — `app/api/` for REST endpoints
- **Parallel fetching** — use Promise.all for independent requests
- **Caching** — leverage Next.js cache, revalidate strategically

```tsx
// Server Component — direct fetch
async function UserProfile({ id }: { id: string }) {
  const user = await getUser(id); // No useEffect needed
  return <div>{user.name}</div>;
}
```

## File Structure

```
app/
├── layout.tsx              # Root layout
├── page.tsx                # Home page
├── globals.css
├── (auth)/                 # Route group (no URL impact)
│   ├── login/page.tsx
│   └── register/page.tsx
├── dashboard/
│   ├── layout.tsx          # Dashboard layout
│   ├── page.tsx            # Dashboard home
│   └── settings/page.tsx
├── api/
│   └── users/route.ts      # API route
└── components/             # Shared components
```

## Performance

- **Image optimization** — always use `next/image`
- **Font optimization** — use `next/font`
- **Metadata** — export metadata object from pages
- **Static generation** — prefer SSG over SSR when possible
- **Edge runtime** — for latency-sensitive routes

## Anti-Patterns

- [FAIL] Using `useEffect` for data fetching in Server Components
- [FAIL] Making everything a Client Component
- [FAIL] Not using `loading.tsx` and `error.tsx`
- [FAIL] Importing server-only code in Client Components
- [FAIL] Not leveraging caching and revalidation

<!-- Source: EXAMPLES.md -->

# nextjs Examples — Anti-patterns vs ContextOS Standard

## Example 1: Server Components vs Client Components

### Anti-pattern: Marking the Entire Page as Client Component

```tsx
// BAD: app/dashboard/page.tsx with 'use client' at top
// Bloats client bundle, loses SEO benefits, eliminates direct DB access
'use client';

export default function DashboardPage() {
  const [data, setData] = useState(null);
  useEffect(() => { fetch('/api/dashboard').then(...) }, []);
  return <div>...</div>;
}
```

### Best practice: ContextOS Standard (RSC by Default, Client Leaf Nodes)

```tsx
// GOOD: Server Component fetches data directly with zero bundle cost
// app/dashboard/page.tsx (Server Component)
import { Suspense } from 'react';
import { db } from '@/lib/db';
import { InteractiveChart } from './InteractiveChart'; // 'use client' leaf component

export default async function DashboardPage() {
  const stats = await db.analytics.getStats();
  return (
    <main>
      <h1>Dashboard</h1>
      <p>Total Revenue: {stats.revenue}</p>
      <Suspense fallback={<ChartSkeleton />}>
        <InteractiveChart initialData={stats.chartData} />
      </Suspense>
    </main>
  );
}
```

<!-- Source: TROUBLESHOOTING.md -->

# nextjs Troubleshooting & Common Mistakes

## 1. Hydration Mismatch Errors

- **Symptom**: "Text content does not match server-rendered HTML".
- **Root Cause**: Rendering dates, window dimensions, or local storage data that differs between server render and client hydration.
- **Fix**: Use suppressHydrationWarning on localized timestamps or load client-only state inside a useEffect after mount.

## 2. Accidental Server Code Bundled to Client

- **Symptom**: "Module not found: Can't resolve 'fs' or 'pg' in client bundle".
- **Root Cause**: Client component importing a utility that transitively imports server-only database code.
- **Fix**: Separate server utilities into *.server.ts and install import 'server-only'; at the top of server files.

## 3. Waterfall Fetches in Server Components

- **Symptom**: Page takes 3 seconds to load due to sequential await statements.
- **Root Cause**: Awaiting independent data sources one after another.
- **Fix**: Use Promise.all([fetchUsers(), fetchProducts()]) or separate into nested <Suspense> boundaries.

