| name | component-builder |
| description | React 19 + Server Component patterns from official Vercel/React docs. Component templates, hooks patterns, RSC boundaries, performance optimization. |
| user-invocable | false |
Component Builder Patterns (Official React 19 + Vercel Source)
Based on React 19.2 docs, Vercel React Best Practices (57 rules), and Next.js Agent Skills.
Server Component (Default)
interface ProductCardProps {
id: string
title: string
price: number
description?: string
}
export default async function ProductCard({ id, title, price, description }: ProductCardProps) {
const inventory = await getInventory(id)
return (
<div className="rounded-lg border p-4">
<h3 className="text-lg font-semibold">{title}</h3>
{description && <p className="text-sm text-muted-foreground">{description}</p>}
<p className="mt-2 text-xl font-bold">${price}</p>
<span className="text-xs text-muted-foreground">
{inventory.count} in stock
</span>
</div>
)
}
Client Component (Interactive)
'use client'
import { useState, useCallback, useTransition } from 'react'
import { useRouter } from 'next/navigation'
interface SearchInputProps {
placeholder?: string
defaultValue?: string
}
export default function SearchInput({
placeholder = 'Search...',
defaultValue = '',
}: SearchInputProps) {
const [query, setQuery] = useState(defaultValue)
const [isPending, startTransition] = useTransition()
const router = useRouter()
const handleSearch = useCallback(
(value: string) => {
setQuery(value)
startTransition(() => {
router.push(`/search?q=${encodeURIComponent(value)}`)
})
},
[router]
)
return (
<input
type="search"
value={query}
onChange={(e) => handleSearch(e.target.value)}
placeholder={placeholder}
className="w-full rounded-md border px-3 py-2"
aria-label="Search"
/>
)
}
Composition Pattern: Server + Client
import InteractiveChart from '@/components/features/interactive-chart'
export default async function DashboardPage() {
const data = await getAnalytics()
return (
<div>
<h1>Dashboard</h1>
<InteractiveChart data={data} /> {/* client handles interactivity */}
</div>
)
}
'use client'
import { useState } from 'react'
interface InteractiveChartProps {
data: AnalyticsData[]
}
export default function InteractiveChart({ data }: InteractiveChartProps) {
const [timeRange, setTimeRange] = useState<'day' | 'week' | 'month'>('week')
const filtered = data.filter((d) => matchesRange(d, timeRange))
return ()
}
React 19 Hooks Reference
useState — State management
const [count, setCount] = useState(0)
const [items, setItems] = useState<Item[]>([])
const increment = useCallback(() => setCount((c) => c + 1), [])
const [data, setData] = useState(() => computeExpensiveDefault())
useCallback — Stable function references
const handleSubmit = useCallback(async (formData: FormData) => {
await submitForm(formData)
}, [])
<button onClick={() => setOpen(true)}>Open</button> // fine as-is
useMemo — Expensive computation caching
const sortedItems = useMemo(
() => items.toSorted((a, b) => a.name.localeCompare(b.name)),
[items]
)
const isActive = status === 'active'
useTransition — Non-urgent updates
const [isPending, startTransition] = useTransition()
function handleTabChange(tab: string) {
startTransition(() => {
setActiveTab(tab)
})
}
useRef — Mutable values without re-renders
const inputRef = useRef<HTMLInputElement>(null)
const scrollPosition = useRef(0)
use() — React 19 context/promise consumer
import { use } from 'react'
function ThemeButton() {
const theme = use(ThemeContext)
return <button className={theme.buttonClass}>Click</button>
}
Heavy Component Loading
import dynamic from 'next/dynamic'
const CodeEditor = dynamic(() => import('@/components/code-editor'), {
ssr: false,
loading: () => <div className="h-96 animate-pulse rounded bg-muted" />,
})
const MapView = dynamic(() => import('@/components/map-view'), {
ssr: false,
loading: () => <div className="h-64 animate-pulse rounded bg-muted" />,
})
Component Rules (Enforced by cs-code-reviewer)
- Default export always — one component per file
- Props interface above component, exported if reused
- No barrel files — import directly from component file path
- Tailwind only — no CSS modules, no styled-components
- Use
cn() from @/lib/utils for conditional classes
- Server Component by default — only
'use client' when needed
- Minimize client boundary — push
'use client' to smallest leaf component
- Parallel fetch in server components —
Promise.all() for independent data
- Suspense boundaries — wrap async children for streaming
- Every component gets a test in
__tests__/unit/components/
Vercel Performance Rules (Top Priority)
| Rule | Impact | Pattern |
|---|
async-parallel | CRITICAL | Promise.all() for independent ops |
bundle-barrel-imports | CRITICAL | Import from specific file, not index |
bundle-dynamic-imports | CRITICAL | next/dynamic for heavy components |
server-serialization | HIGH | Minimize data passed to client components |
rerender-memo | MEDIUM | Extract expensive work into memoized components |
rerender-derived-state | MEDIUM | Subscribe to derived booleans, not raw values |
rendering-conditional-render | MEDIUM | Use ternary ? :, not && |