templates/skills/react-best-practices/SKILL.md
React and Next.js performance optimization patterns. Use BEFORE implementing any React code to ensure best practices are followed.
npx skillsauth add avivk5498/the-claude-protocol react-best-practicesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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])
}
testing
Core engineering principles for implementation tasks
development
Invoke at the start of any implementation task to enforce verification-first development
tools
Bootstrap lean multi-agent orchestration with beads task tracking. Use for projects needing agent delegation without heavy MCP overhead.
tools
Bootstrap lean multi-agent orchestration with beads task tracking. Use for projects needing agent delegation without heavy MCP overhead.