com um clique
react-best-practices
// React and Next.js performance optimization patterns. Use BEFORE implementing any React code to ensure best practices are followed.
// React and Next.js performance optimization patterns. Use BEFORE implementing any React code to ensure best practices are followed.
Bootstrap lean multi-agent orchestration with beads task tracking. Use for projects needing agent delegation without heavy MCP overhead.
Bootstrap lean multi-agent orchestration with beads task tracking. Use for projects needing agent delegation without heavy MCP overhead.
Core engineering principles for implementation tasks
Invoke at the start of any implementation task to enforce verification-first development
| name | react-best-practices |
| description | React and Next.js performance optimization patterns. Use BEFORE implementing any React code to ensure best practices are followed. |
Version 1.0.0 Source: Vercel Engineering (vercel-labs/agent-skills)
Note: This document is for agents and LLMs to follow when maintaining, generating, or refactoring React and Next.js codebases. Contains 40+ rules across 8 categories, prioritized by impact.
Before implementing ANY React/Next.js code:
Priority order: Eliminating Waterfalls > Bundle Size > Server-Side > Client-Side > Re-renders > Rendering > JS Perf > Advanced
Impact: CRITICAL - Waterfalls are the #1 performance killer.
Move await into branches where actually used.
// BAD: blocks both branches
async function handleRequest(userId: string, skipProcessing: boolean) {
const userData = await fetchUserData(userId)
if (skipProcessing) return { skipped: true }
return processUserData(userData)
}
// GOOD: only blocks when needed
async function handleRequest(userId: string, skipProcessing: boolean) {
if (skipProcessing) return { skipped: true }
const userData = await fetchUserData(userId)
return processUserData(userData)
}
// BAD: 3 round trips
const user = await fetchUser()
const posts = await fetchPosts()
const comments = await fetchComments()
// GOOD: 1 round trip
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
])
// BAD: wrapper blocked by data
async function Page() {
const data = await fetchData()
return (
<div>
<Sidebar />
<DataDisplay data={data} />
<Footer />
</div>
)
}
// GOOD: wrapper shows immediately
function Page() {
return (
<div>
<Sidebar />
<Suspense fallback={<Skeleton />}>
<DataDisplay />
</Suspense>
<Footer />
</div>
)
}
Impact: CRITICAL - Reduces TTI and LCP.
// BAD: loads 1,583 modules
import { Check, X, Menu } from 'lucide-react'
// GOOD: loads only 3 modules
import Check from 'lucide-react/dist/esm/icons/check'
import X from 'lucide-react/dist/esm/icons/x'
import Menu from 'lucide-react/dist/esm/icons/menu'
// ALTERNATIVE: Next.js 13.5+ config
// next.config.js
module.exports = {
experimental: {
optimizePackageImports: ['lucide-react', '@mui/material']
}
}
// BAD: Monaco bundles with main chunk (~300KB)
import { MonacoEditor } from './monaco-editor'
// GOOD: Monaco loads on demand
import dynamic from 'next/dynamic'
const MonacoEditor = dynamic(
() => import('./monaco-editor').then(m => m.MonacoEditor),
{ ssr: false }
)
// BAD: blocks initial bundle
import { Analytics } from '@vercel/analytics/react'
// GOOD: loads after hydration
import dynamic from 'next/dynamic'
const Analytics = dynamic(
() => import('@vercel/analytics/react').then(m => m.Analytics),
{ ssr: false }
)
function EditorButton({ onClick }: { onClick: () => void }) {
const preload = () => {
if (typeof window !== 'undefined') {
void import('./monaco-editor')
}
}
return (
<button onMouseEnter={preload} onFocus={preload} onClick={onClick}>
Open Editor
</button>
)
}
Impact: HIGH
// BAD: serializes all 50 fields
async function Page() {
const user = await fetchUser() // 50 fields
return <Profile user={user} />
}
// GOOD: serializes only needed fields
async function Page() {
const user = await fetchUser()
return <Profile name={user.name} avatar={user.avatar} />
}
// BAD: Sidebar waits for Header's fetch
export default async function Page() {
const header = await fetchHeader()
return (
<div>
<div>{header}</div>
<Sidebar />
</div>
)
}
// GOOD: both fetch simultaneously
async function Header() {
const data = await fetchHeader()
return <div>{data}</div>
}
async function Sidebar() {
const items = await fetchSidebarItems()
return <nav>{items.map(renderItem)}</nav>
}
export default function Page() {
return (
<div>
<Header />
<Sidebar />
</div>
)
}
import { cache } from 'react'
export const getCurrentUser = cache(async () => {
const session = await auth()
if (!session?.user?.id) return null
return await db.user.findUnique({ where: { id: session.user.id } })
})
import { after } from 'next/server'
export async function POST(request: Request) {
await updateDatabase(request)
// Log after response is sent
after(async () => {
const userAgent = (await headers()).get('user-agent')
logUserAction({ userAgent })
})
return Response.json({ status: 'success' })
}
Impact: MEDIUM-HIGH
// BAD: no deduplication
function UserList() {
const [users, setUsers] = useState([])
useEffect(() => {
fetch('/api/users').then(r => r.json()).then(setUsers)
}, [])
}
// GOOD: multiple instances share one request
import useSWR from 'swr'
function UserList() {
const { data: users } = useSWR('/api/users', fetcher)
}
Impact: MEDIUM
// BAD: requires state as dependency, risk of stale closure
const addItems = useCallback((newItems: Item[]) => {
setItems([...items, ...newItems])
}, [items])
// GOOD: stable callback, no stale closures
const addItems = useCallback((newItems: Item[]) => {
setItems(curr => [...curr, ...newItems])
}, [])
// BAD: runs on every render
const [settings] = useState(JSON.parse(localStorage.getItem('settings') || '{}'))
// GOOD: runs only once
const [settings] = useState(() => {
const stored = localStorage.getItem('settings')
return stored ? JSON.parse(stored) : {}
})
import { startTransition } from 'react'
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0)
useEffect(() => {
const handler = () => {
startTransition(() => setScrollY(window.scrollY))
}
window.addEventListener('scroll', handler, { passive: true })
return () => window.removeEventListener('scroll', handler)
}, [])
}
// BAD: re-runs on any user field change
useEffect(() => {
console.log(user.id)
}, [user])
// GOOD: re-runs only when id changes
useEffect(() => {
console.log(user.id)
}, [user.id])
Impact: MEDIUM
.message-item {
content-visibility: auto;
contain-intrinsic-size: 0 80px;
}
// BAD: recreates element every render
function Container() {
return loading && <div className="animate-pulse h-20 bg-gray-200" />
}
// GOOD: reuses same element
const loadingSkeleton = <div className="animate-pulse h-20 bg-gray-200" />
function Container() {
return loading && loadingSkeleton
}
// BAD: no hardware acceleration
<svg className="animate-spin">...</svg>
// GOOD: hardware accelerated
<div className="animate-spin">
<svg>...</svg>
</div>
Impact: LOW-MEDIUM
// BAD: O(n) per lookup
items.filter(item => allowedIds.includes(item.id))
// GOOD: O(1) per lookup
const allowedSet = new Set(allowedIds)
items.filter(item => allowedSet.has(item.id))
// BAD: mutates original array
const sorted = users.sort((a, b) => a.name.localeCompare(b.name))
// GOOD: creates new array
const sorted = users.toSorted((a, b) => a.name.localeCompare(b.name))
// BAD: processes all items after finding error
function validateUsers(users: User[]) {
let hasError = false
for (const user of users) {
if (!user.email) hasError = true
}
return hasError ? { valid: false } : { valid: true }
}
// GOOD: returns immediately on first error
function validateUsers(users: User[]) {
for (const user of users) {
if (!user.email) return { valid: false, error: 'Email required' }
}
return { valid: true }
}
Impact: LOW
import { useEffectEvent } from 'react'
function useWindowEvent(event: string, handler: () => void) {
const onEvent = useEffectEvent(handler)
useEffect(() => {
window.addEventListener(event, onEvent)
return () => window.removeEventListener(event, onEvent)
}, [event])
}