Plugins: automacao, crm-ops, design-media, dev-tools, gestao, infraestrutura, marketing, negocio, perfex-dev, project-manager, wordpress + hello-plugin (existente). Totais: 83 skills, 44 agents, 12 datasets.json Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
723 lines
17 KiB
Markdown
723 lines
17 KiB
Markdown
---
|
|
name: nextjs
|
|
description: Next.js development best practices and patterns. App router, server components,
|
|
API routes, and deployment. Use when user mentions "nextjs", "next.js", "react server",
|
|
"app router", "next deployment".
|
|
author: Descomplicar® Crescimento Digital
|
|
version: 2.0.0
|
|
quality_score: 75
|
|
user_invocable: true
|
|
allowed-tools: Glob
|
|
---
|
|
|
|
# /nextjs - Next.js Development
|
|
|
|
Desenvolvimento Next.js moderno (13+) com App Router e Server Components.
|
|
|
|
## Quando Usar
|
|
|
|
- Criar aplicações Next.js
|
|
- Migrar de Pages para App Router
|
|
- Implementar Server Components
|
|
- Configurar Server Actions
|
|
- Optimizar SEO e performance
|
|
|
|
## App Router Structure
|
|
|
|
```
|
|
app/
|
|
├── layout.tsx # Root layout (obrigatório)
|
|
├── page.tsx # Home page
|
|
├── loading.tsx # Loading UI (Suspense)
|
|
├── error.tsx # Error boundary
|
|
├── not-found.tsx # 404 page
|
|
├── globals.css # Global styles
|
|
├── (auth)/ # Route group (não afecta URL)
|
|
│ ├── login/page.tsx
|
|
│ └── register/page.tsx
|
|
├── dashboard/
|
|
│ ├── layout.tsx # Nested layout
|
|
│ ├── page.tsx
|
|
│ └── [id]/
|
|
│ └── page.tsx # Dynamic route
|
|
└── api/
|
|
└── route.ts # API route
|
|
```
|
|
|
|
## Server vs Client Components
|
|
|
|
### Server Component (default)
|
|
|
|
```tsx
|
|
// ✅ PADRÃO - Runs on server
|
|
async function ProductsPage() {
|
|
// Pode fazer fetch directo
|
|
const products = await db.product.findMany();
|
|
|
|
return (
|
|
<div>
|
|
{products.map(p => (
|
|
<ProductCard key={p.id} product={p} />
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default ProductsPage;
|
|
```
|
|
|
|
**Vantagens:**
|
|
- Acesso directo a BD
|
|
- Menos JavaScript no cliente
|
|
- SEO melhor
|
|
- Dados sempre frescos
|
|
|
|
### Client Component
|
|
|
|
```tsx
|
|
'use client';
|
|
|
|
import { useState } from 'react';
|
|
|
|
// ✅ Para interactividade
|
|
function Counter() {
|
|
const [count, setCount] = useState(0);
|
|
|
|
return (
|
|
<button onClick={() => setCount(c => c + 1)}>
|
|
{count}
|
|
</button>
|
|
);
|
|
}
|
|
```
|
|
|
|
**Quando usar:**
|
|
- useState, useEffect, useContext
|
|
- Event listeners (onClick, onChange)
|
|
- Browser APIs (localStorage, window)
|
|
- Custom hooks
|
|
|
|
## Data Fetching Patterns
|
|
|
|
### 1. Static (SSG)
|
|
|
|
```tsx
|
|
// Default behaviour - cache infinito
|
|
async function Page() {
|
|
const data = await fetch('https://api.example.com/data', {
|
|
cache: 'force-cache' // Explícito (mas é default)
|
|
});
|
|
|
|
return <div>{data.title}</div>;
|
|
}
|
|
```
|
|
|
|
### 2. Dynamic (SSR)
|
|
|
|
```tsx
|
|
// Sempre fetch fresco
|
|
async function Page() {
|
|
const data = await fetch('https://api.example.com/data', {
|
|
cache: 'no-store' // NUNCA cache
|
|
});
|
|
|
|
return <div>{data.title}</div>;
|
|
}
|
|
```
|
|
|
|
### 3. Revalidate (ISR)
|
|
|
|
```tsx
|
|
// Cache com revalidação automática
|
|
async function Page() {
|
|
const data = await fetch('https://api.example.com/data', {
|
|
next: { revalidate: 60 } // Revalida a cada 60s
|
|
});
|
|
|
|
return <div>{data.title}</div>;
|
|
}
|
|
```
|
|
|
|
## Server Actions
|
|
|
|
```tsx
|
|
// app/actions.ts
|
|
'use server';
|
|
|
|
import { revalidatePath } from 'next/cache';
|
|
import { db } from '@/lib/db';
|
|
|
|
export async function createPost(formData: FormData) {
|
|
const title = formData.get('title') as string;
|
|
const content = formData.get('content') as string;
|
|
|
|
await db.post.create({
|
|
data: { title, content }
|
|
});
|
|
|
|
revalidatePath('/posts');
|
|
}
|
|
|
|
// app/create-post/page.tsx
|
|
import { createPost } from '@/app/actions';
|
|
|
|
export default function CreatePost() {
|
|
return (
|
|
<form action={createPost}>
|
|
<input name="title" required />
|
|
<textarea name="content" required />
|
|
<button type="submit">Create</button>
|
|
</form>
|
|
);
|
|
}
|
|
```
|
|
|
|
## Layouts e Loading States
|
|
|
|
### Root Layout
|
|
|
|
```tsx
|
|
// app/layout.tsx
|
|
import './globals.css';
|
|
import { Inter } from 'next/font/google';
|
|
|
|
const inter = Inter({ subsets: ['latin'] });
|
|
|
|
export const metadata = {
|
|
title: 'My App',
|
|
description: 'Description'
|
|
};
|
|
|
|
export default function RootLayout({
|
|
children
|
|
}: {
|
|
children: React.ReactNode
|
|
}) {
|
|
return (
|
|
<html lang="pt">
|
|
<body className={inter.className}>
|
|
<nav>...</nav>
|
|
{children}
|
|
<footer>...</footer>
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Loading UI
|
|
|
|
```tsx
|
|
// app/dashboard/loading.tsx
|
|
export default function Loading() {
|
|
return <div>Loading...</div>;
|
|
}
|
|
|
|
// Wrapper automático em <Suspense>
|
|
```
|
|
|
|
### Error Handling
|
|
|
|
```tsx
|
|
// app/error.tsx
|
|
'use client';
|
|
|
|
export default function Error({
|
|
error,
|
|
reset
|
|
}: {
|
|
error: Error;
|
|
reset: () => void;
|
|
}) {
|
|
return (
|
|
<div>
|
|
<h2>Something went wrong!</h2>
|
|
<button onClick={reset}>Try again</button>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
## Routing Avançado
|
|
|
|
### Parallel Routes
|
|
|
|
```tsx
|
|
// app/layout.tsx
|
|
export default function Layout({
|
|
children,
|
|
modal,
|
|
sidebar
|
|
}: {
|
|
children: React.ReactNode;
|
|
modal: React.ReactNode;
|
|
sidebar: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<>
|
|
{sidebar}
|
|
{children}
|
|
{modal}
|
|
</>
|
|
);
|
|
}
|
|
|
|
// app/@sidebar/page.tsx
|
|
// app/@modal/page.tsx
|
|
```
|
|
|
|
### Intercepting Routes
|
|
|
|
```tsx
|
|
// app/feed/page.tsx - Lista de fotos
|
|
// app/photo/[id]/page.tsx - Página completa da foto
|
|
// app/@modal/(.)photo/[id]/page.tsx - Modal da foto (intercepta)
|
|
|
|
// Navegar via <Link> abre modal
|
|
// Refresh ou URL directo abre página completa
|
|
```
|
|
|
|
## Middleware
|
|
|
|
```tsx
|
|
// middleware.ts
|
|
import { NextResponse } from 'next/server';
|
|
import type { NextRequest } from 'next/server';
|
|
|
|
export function middleware(request: NextRequest) {
|
|
const token = request.cookies.get('token');
|
|
|
|
// Auth guard
|
|
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
|
|
return NextResponse.redirect(new URL('/login', request.url));
|
|
}
|
|
|
|
// Custom header
|
|
const response = NextResponse.next();
|
|
response.headers.set('x-custom-header', 'value');
|
|
return response;
|
|
}
|
|
|
|
export const config = {
|
|
matcher: ['/dashboard/:path*', '/api/:path*']
|
|
};
|
|
```
|
|
|
|
## Performance Rules (Vercel Engineering)
|
|
|
|
57 regras priorizadas por impacto. Fonte: Vercel Engineering react-best-practices.
|
|
|
|
### 1. Eliminating Waterfalls (CRITICAL)
|
|
|
|
```tsx
|
|
// ❌ ERRADO: Awaits sequenciais (waterfall)
|
|
const session = await getSession()
|
|
const config = await getConfig()
|
|
const user = await getUser(session.userId)
|
|
|
|
// ✅ CORRECTO: Paralelo com Promise.all()
|
|
const [session, config] = await Promise.all([
|
|
getSession(),
|
|
getConfig()
|
|
])
|
|
const user = await getUser(session.userId) // depende de session
|
|
|
|
// ✅ CORRECTO: Suspense boundaries estratégicos
|
|
async function Page() {
|
|
return (
|
|
<Layout>
|
|
<Suspense fallback={<HeaderSkeleton />}>
|
|
<Header />
|
|
</Suspense>
|
|
<Suspense fallback={<ContentSkeleton />}>
|
|
<SlowContent />
|
|
</Suspense>
|
|
</Layout>
|
|
)
|
|
}
|
|
|
|
// ✅ CORRECTO: Defer await até necessário
|
|
async function handler(req) {
|
|
const dataPromise = fetchData() // inicia imediatamente
|
|
if (req.method === 'HEAD') return new Response() // early return sem await
|
|
const data = await dataPromise // só espera quando precisa
|
|
return Response.json(data)
|
|
}
|
|
```
|
|
|
|
**Regras:**
|
|
- `async-defer-await`: Mover await para onde é realmente necessário
|
|
- `async-dependency-parallel`: Usar `better-all` para dependências parciais
|
|
- `async-prevent-waterfall`: Iniciar promises antes de awaitar
|
|
- `async-promise-all`: `Promise.all()` para operações independentes
|
|
- `async-suspense-boundaries`: Suspense para UI progressiva
|
|
|
|
### 2. Bundle Size (CRITICAL)
|
|
|
|
```tsx
|
|
// ❌ ERRADO: Barrel file imports
|
|
import { Button, Icon } from '@mui/material' // carrega tudo
|
|
|
|
// ✅ CORRECTO: Import directo
|
|
import Button from '@mui/material/Button'
|
|
|
|
// ✅ CORRECTO: optimizePackageImports (Next.js 13.5+)
|
|
// next.config.js
|
|
module.exports = {
|
|
experimental: {
|
|
optimizePackageImports: ['lucide-react', '@mui/material', 'lodash']
|
|
}
|
|
}
|
|
|
|
// ✅ CORRECTO: Dynamic imports para componentes pesados
|
|
const MonacoEditor = dynamic(() => import('./MonacoEditor'), {
|
|
ssr: false,
|
|
loading: () => <EditorSkeleton />
|
|
})
|
|
|
|
// ✅ CORRECTO: Preload em hover/focus
|
|
function FeatureButton() {
|
|
return (
|
|
<button
|
|
onMouseEnter={() => import('./HeavyFeature')}
|
|
onClick={openFeature}
|
|
>
|
|
Open Feature
|
|
</button>
|
|
)
|
|
}
|
|
```
|
|
|
|
**Regras:**
|
|
- `bundle-barrel-imports`: Evitar barrel files (index.js re-exports)
|
|
- `bundle-conditional-loading`: Carregar módulos só quando feature activa
|
|
- `bundle-defer-third-party`: `next/dynamic` com `ssr: false` para analytics
|
|
- `bundle-dynamic-imports`: Lazy-load componentes pesados (>50KB)
|
|
- `bundle-preload-intent`: Preload em hover para UX instantânea
|
|
|
|
### 3. Server-Side Performance (HIGH)
|
|
|
|
```tsx
|
|
// ✅ Server Actions: autenticar SEMPRE (são endpoints públicos!)
|
|
'use server'
|
|
async function updateProfile(formData: FormData) {
|
|
const session = await auth() // 1. Autenticar
|
|
if (!session) throw new Error('Unauthorized')
|
|
const data = schema.parse(formData) // 2. Validar input
|
|
await db.user.update({ where: { id: session.userId }, data }) // 3. Executar
|
|
revalidatePath('/profile')
|
|
}
|
|
|
|
// ✅ Minimizar serialização RSC - só campos necessários
|
|
async function UserCard({ userId }: { userId: string }) {
|
|
const user = await db.user.findUnique({ where: { id: userId } })
|
|
// Passa só o necessário, não o objecto inteiro com 50+ campos
|
|
return <ClientCard name={user.name} avatar={user.avatar} />
|
|
}
|
|
|
|
// ✅ React.cache() para deduplicação per-request
|
|
const getUser = cache(async (id: string) => {
|
|
return db.user.findUnique({ where: { id } })
|
|
})
|
|
|
|
// ✅ after() para operações não-bloqueantes
|
|
import { after } from 'next/server'
|
|
async function submitForm(data: FormData) {
|
|
const result = await processForm(data)
|
|
after(async () => {
|
|
await logAnalytics(result) // executa APÓS resposta enviada
|
|
})
|
|
return result
|
|
}
|
|
```
|
|
|
|
**Regras:**
|
|
- `server-auth-actions`: Autenticar Server Actions como API routes
|
|
- `server-avoid-duplicate-serialization`: Transformar dados no cliente
|
|
- `server-lru-cache`: LRUCache para dados cross-request
|
|
- `server-minimize-serialization`: Só campos necessários em RSC props
|
|
- `server-parallel-composition`: Composição paralela de Server Components
|
|
- `server-react-cache`: `React.cache()` para deduplicação per-request
|
|
- `server-after`: `after()` para logging/analytics não-bloqueante
|
|
|
|
### 4. Client-Side Data Fetching (MEDIUM-HIGH)
|
|
|
|
```tsx
|
|
// ✅ SWR para deduplicação automática
|
|
const { data, isLoading } = useSWR('/api/user', fetcher)
|
|
|
|
// ✅ Passive event listeners para scroll performance
|
|
element.addEventListener('scroll', handler, { passive: true })
|
|
|
|
// ✅ localStorage versionado com try-catch
|
|
const key = 'userConfig:v2'
|
|
try {
|
|
const data = JSON.parse(localStorage.getItem(key) ?? '{}')
|
|
} catch { /* incognito mode, quota exceeded */ }
|
|
```
|
|
|
|
### 5. Re-render Optimization (MEDIUM)
|
|
|
|
```tsx
|
|
// ✅ Derived state: calcular durante render, NÃO em useEffect
|
|
function FilteredList({ items, query }: Props) {
|
|
const filtered = items.filter(i => i.name.includes(query)) // derive!
|
|
return <List items={filtered} />
|
|
}
|
|
|
|
// ✅ Functional setState para evitar stale closures
|
|
setItems(curr => [...curr, ...newItems]) // ✅
|
|
// setItems([...items, ...newItems]) // ❌ stale closure
|
|
|
|
// ✅ Lazy state initialization
|
|
const [index] = useState(() => buildSearchIndex(items)) // computa 1x
|
|
|
|
// ✅ useTransition para updates não-urgentes
|
|
const [isPending, startTransition] = useTransition()
|
|
startTransition(() => setSearchResults(results))
|
|
|
|
// ✅ Extrair defaults não-primitivos para module scope
|
|
const EMPTY_ARRAY: string[] = [] // ✅ fora do componente
|
|
const NOOP = () => {} // ✅ fora do componente
|
|
function Component({ items = EMPTY_ARRAY, onClick = NOOP }) { ... }
|
|
|
|
// ✅ Dependências de effect: primitivos, não objectos
|
|
useEffect(() => { fetchUser(userId) }, [userId]) // ✅ primitivo
|
|
// useEffect(() => { ... }, [user]) // ❌ objecto = nova ref cada render
|
|
```
|
|
|
|
### 6. Rendering Performance (MEDIUM)
|
|
|
|
```tsx
|
|
// ✅ CSS content-visibility para listas longas
|
|
// .message { content-visibility: auto; contain-intrinsic-size: 0 80px; }
|
|
|
|
// ✅ Hoist static JSX fora do componente
|
|
const skeleton = <div className="skeleton h-4 w-full" /> // module scope
|
|
|
|
// ✅ Conditional rendering explícito (evitar && com falsy)
|
|
{count > 0 ? <Badge>{count}</Badge> : null} // ✅
|
|
// {count && <Badge>{count}</Badge>} // ❌ renderiza "0"
|
|
|
|
// ✅ useTransition em vez de useState manual para loading
|
|
const [isPending, startTransition] = useTransition()
|
|
// const [isLoading, setIsLoading] = useState(false) // ❌ manual
|
|
|
|
// ✅ Hydration: inline script para evitar flicker
|
|
// <script dangerouslySetInnerHTML={{ __html: `
|
|
// document.documentElement.classList.add(
|
|
// localStorage.getItem('theme') === 'dark' ? 'dark' : 'light'
|
|
// )
|
|
// `}} />
|
|
```
|
|
|
|
### 7. JavaScript Performance (LOW-MEDIUM)
|
|
|
|
```tsx
|
|
// ✅ Index Maps para lookups repetidos O(1) vs O(n)
|
|
const userMap = new Map(users.map(u => [u.id, u]))
|
|
orders.map(o => ({ ...o, user: userMap.get(o.userId) }))
|
|
|
|
// ✅ Set para membership checks
|
|
const allowedIds = new Set(['a', 'b', 'c'])
|
|
if (allowedIds.has(id)) { ... } // O(1) vs .includes() O(n)
|
|
|
|
// ✅ toSorted() para imutabilidade (React-safe)
|
|
const sorted = items.toSorted((a, b) => a.name.localeCompare(b.name))
|
|
// items.sort() // ❌ muta o array original
|
|
|
|
// ✅ Early return para evitar processamento desnecessário
|
|
function validate(items: Item[]) {
|
|
for (const item of items) {
|
|
if (!item.valid) return { error: item.id } // sai cedo
|
|
}
|
|
return { success: true }
|
|
}
|
|
|
|
// ✅ Combinar iterações de array
|
|
const { active, expired } = items.reduce((acc, item) => {
|
|
if (item.active) acc.active.push(item)
|
|
else acc.expired.push(item)
|
|
return acc
|
|
}, { active: [], expired: [] })
|
|
// Em vez de items.filter(active) + items.filter(expired) = 2 iterações
|
|
```
|
|
|
|
### 8. Advanced Patterns (LOW)
|
|
|
|
```tsx
|
|
// ✅ Init uma vez, não por mount (Strict Mode remonta)
|
|
let didInit = false
|
|
function App() {
|
|
useEffect(() => {
|
|
if (didInit) return
|
|
didInit = true
|
|
initializeAnalytics()
|
|
}, [])
|
|
}
|
|
|
|
// ✅ useEffectEvent para callbacks estáveis em effects
|
|
const onTick = useEffectEvent((value) => {
|
|
console.log(value) // sempre valor mais recente
|
|
})
|
|
useEffect(() => {
|
|
const id = setInterval(() => onTick(count), 1000)
|
|
return () => clearInterval(id)
|
|
}, []) // sem dependência de count!
|
|
```
|
|
|
|
### Resumo de Impacto
|
|
|
|
| Prioridade | Categoria | Regras | Ganho |
|
|
|---|---|---|---|
|
|
| CRITICAL | Waterfalls | 5 | 2-10x latência |
|
|
| CRITICAL | Bundle Size | 5 | 200-800ms load |
|
|
| HIGH | Server-Side | 7 | RSC payload, TTI |
|
|
| MEDIUM-HIGH | Client Fetch | 4 | Request dedup |
|
|
| MEDIUM | Re-renders | 12 | UI responsiveness |
|
|
| MEDIUM | Rendering | 9 | Paint performance |
|
|
| LOW-MEDIUM | JS Perf | 12 | CPU cycles |
|
|
| LOW | Advanced | 3 | Init correctness |
|
|
|
|
---
|
|
|
|
## Optimizações
|
|
|
|
### Images
|
|
|
|
```tsx
|
|
import Image from 'next/image';
|
|
|
|
<Image
|
|
src="/photo.jpg"
|
|
width={500}
|
|
height={300}
|
|
alt="Photo"
|
|
priority // LCP image
|
|
placeholder="blur"
|
|
blurDataURL="data:image/..."
|
|
/>
|
|
```
|
|
|
|
### Fonts
|
|
|
|
```tsx
|
|
import { Inter, Roboto_Mono } from 'next/font/google';
|
|
|
|
const inter = Inter({
|
|
subsets: ['latin'],
|
|
display: 'swap',
|
|
});
|
|
|
|
const robotoMono = Roboto_Mono({
|
|
subsets: ['latin'],
|
|
weight: ['400', '700'],
|
|
});
|
|
```
|
|
|
|
### Metadata (SEO)
|
|
|
|
```tsx
|
|
// app/page.tsx
|
|
export const metadata = {
|
|
title: 'Home',
|
|
description: 'Home page',
|
|
openGraph: {
|
|
title: 'Home',
|
|
description: 'Home page',
|
|
images: ['/og-image.jpg'],
|
|
},
|
|
twitter: {
|
|
card: 'summary_large_image',
|
|
},
|
|
};
|
|
```
|
|
|
|
## Deployment
|
|
|
|
### Vercel (recomendado)
|
|
|
|
```bash
|
|
npm install -g vercel
|
|
vercel
|
|
```
|
|
|
|
### Docker
|
|
|
|
```dockerfile
|
|
FROM node:18-alpine AS builder
|
|
WORKDIR /app
|
|
COPY package*.json ./
|
|
RUN npm ci
|
|
COPY . .
|
|
RUN npm run build
|
|
|
|
FROM node:18-alpine
|
|
WORKDIR /app
|
|
COPY --from=builder /app/.next ./.next
|
|
COPY --from=builder /app/public ./public
|
|
COPY --from=builder /app/package*.json ./
|
|
RUN npm ci --production
|
|
EXPOSE 3000
|
|
CMD ["npm", "start"]
|
|
```
|
|
|
|
### Static Export
|
|
|
|
```js
|
|
// next.config.js
|
|
module.exports = {
|
|
output: 'export',
|
|
trailingSlash: true,
|
|
};
|
|
```
|
|
|
|
```bash
|
|
npm run build
|
|
# Output: out/
|
|
```
|
|
|
|
## Datasets Dify
|
|
|
|
| Dataset | ID | Prioridade |
|
|
|---------|----|-----------:|
|
|
| **Desenvolvimento de Software** | `e7c7decc-0ded-4351-ab14-b110b3c38ec9` | 1 |
|
|
| **TI (Tecnologia da Informação)** | `7f63ec0c-6321-488c-b107-980140199850` | 1 |
|
|
|
|
---
|
|
|
|
**Versão**: 1.0.0 | **Autor**: Descomplicar®
|
|
|
|
---
|
|
|
|
|
|
## Quando NÃO Usar
|
|
|
|
- Para tarefas fora do domínio de especialização desta skill
|
|
- Quando outra skill mais específica está disponível
|
|
- Para operações que requerem confirmação manual do utilizador
|
|
|
|
|
|
## Protocolo
|
|
|
|
1. Analisar requisitos da tarefa
|
|
2. Verificar disponibilidade de ferramentas necessárias
|
|
3. Executar operações de forma incremental
|
|
4. Validar resultados antes de concluir
|
|
5. Reportar status e próximos passos
|
|
|
|
|
|
## Exemplos
|
|
|
|
### Exemplo 1: Uso Básico
|
|
```
|
|
Input: [descrição da tarefa]
|
|
Output: [resultado esperado]
|
|
```
|
|
|
|
### Exemplo 2: Uso Avançado
|
|
```
|
|
Input: [caso complexo]
|
|
Output: [resultado detalhado]
|
|
```
|