Component Development
Best practices for building React components, patterns, and reusable UI elements.
Component Development
Component Patterns
Server Components (Default)
Server Components are the default in Next.js App Router and should be used whenever possible for better performance:
// Server component for data fetching and static content
export default async function InventoryPage() {
// Data fetching happens on the server
const stats = await getInventoryStats()
const products = await getProducts()
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<StatsCard title="Total Products" value={stats.totalProducts} />
<StatsCard title="Total Value" value={formatCurrency(stats.totalValue)} />
<StatsCard title="Low Stock" value={stats.lowStockItems} />
<StatsCard title="Out of Stock" value={stats.outOfStockItems} />
</div>
<Suspense fallback={<TableSkeleton />}>
<InventoryTable products={products} />
</Suspense>
</div>
)
}
// Simple server component
function StatsCard({ title, value }: { title: string; value: string | number }) {
return (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
{title}
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
</CardContent>
</Card>
)
}
Client Components
Use Client Components only when you need interactivity, browser APIs, or React hooks:
'use client'
import { useState, useEffect } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
interface ProductSearchProps {
onSearch: (query: string) => void
initialQuery?: string
}
export function ProductSearch({ onSearch, initialQuery = '' }: ProductSearchProps) {
const [query, setQuery] = useState(initialQuery)
const [isSearching, setIsSearching] = useState(false)
// Debounced search
useEffect(() => {
if (!query) return
setIsSearching(true)
const timeoutId = setTimeout(() => {
onSearch(query)
setIsSearching(false)
}, 300)
return () => clearTimeout(timeoutId)
}, [query, onSearch])
const handleClear = () => {
setQuery('')
onSearch('')
}
return (
<div className="flex gap-2">
<Input
type="text"
placeholder="Search products..."
value={query}
onChange={(e) => setQuery(e.target.value)}
className="flex-1"
/>
{query && (
<Button
variant="outline"
onClick={handleClear}
disabled={isSearching}
>
Clear
</Button>
)}
</div>
)
}
Reusable UI Components
Build flexible, reusable components with proper TypeScript interfaces:
// Generic data table component
interface Column<T> {
key: keyof T
header: string
render?: (value: T[keyof T], item: T) => React.ReactNode
sortable?: boolean
}
interface DataTableProps<T> {
data: T[]
columns: Column<T>[]
searchKey?: keyof T
loading?: boolean
onRowClick?: (item: T) => void
className?: string
}
export function DataTable<T extends Record<string, any>>({
data,
columns,
searchKey,
loading = false,
onRowClick,
className
}: DataTableProps<T>) {
const [sortColumn, setSortColumn] = useState<keyof T | null>(null)
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc')
const [searchTerm, setSearchTerm] = useState('')
const filteredData = useMemo(() => {
if (!searchKey || !searchTerm) return data
return data.filter(item =>
String(item[searchKey])
.toLowerCase()
.includes(searchTerm.toLowerCase())
)
}, [data, searchKey, searchTerm])
const sortedData = useMemo(() => {
if (!sortColumn) return filteredData
return [...filteredData].sort((a, b) => {
const aVal = a[sortColumn]
const bVal = b[sortColumn]
if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1
if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1
return 0
})
}, [filteredData, sortColumn, sortDirection])
const handleSort = (column: keyof T) => {
if (sortColumn === column) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc')
} else {
setSortColumn(column)
setSortDirection('asc')
}
}
if (loading) {
return <TableSkeleton />
}
return (
<div className={cn('space-y-4', className)}>
{searchKey && (
<Input
placeholder={`Search by ${String(searchKey)}...`}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="max-w-sm"
/>
)}
<Table>
<TableHeader>
<TableRow>
{columns.map((column) => (
<TableHead
key={String(column.key)}
className={column.sortable ? 'cursor-pointer hover:bg-muted' : ''}
onClick={() => column.sortable && handleSort(column.key)}
>
<div className="flex items-center gap-2">
{column.header}
{column.sortable && sortColumn === column.key && (
<ArrowUpDown className="h-4 w-4" />
)}
</div>
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{sortedData.map((item, index) => (
<TableRow
key={index}
className={onRowClick ? 'cursor-pointer hover:bg-muted/50' : ''}
onClick={() => onRowClick?.(item)}
>
{columns.map((column) => (
<TableCell key={String(column.key)}>
{column.render
? column.render(item[column.key], item)
: String(item[column.key])
}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
)
}
Component Best Practices
Error Boundaries
Implement error boundaries to gracefully handle component errors:
'use client'
import React from 'react'
import { Button } from '@/components/ui/button'
import { AlertTriangle } from 'lucide-react'
interface ErrorBoundaryState {
hasError: boolean
error?: Error
}
export class ErrorBoundary extends React.Component<
React.PropsWithChildren<{}>,
ErrorBoundaryState
> {
constructor(props: React.PropsWithChildren<{}>) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error }
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Component error:', error, errorInfo)
// Log error to monitoring service
this.logErrorToService(error, errorInfo)
}
private logErrorToService = (error: Error, errorInfo: React.ErrorInfo) => {
// Send error to monitoring service like Sentry
}
private handleReset = () => {
this.setState({ hasError: false, error: undefined })
}
render() {
if (this.state.hasError) {
return <ErrorFallback error={this.state.error} onReset={this.handleReset} />
}
return this.props.children
}
}
function ErrorFallback({ error, onReset }: { error?: Error; onReset: () => void }) {
return (
<div className="flex flex-col items-center justify-center p-8 text-center">
<AlertTriangle className="h-12 w-12 text-destructive mb-4" />
<h2 className="text-lg font-semibold mb-2">Something went wrong</h2>
<p className="text-muted-foreground mb-4">
{error?.message || 'An unexpected error occurred'}
</p>
<Button onClick={onReset} variant="outline">
Try again
</Button>
</div>
)
}
Loading States
Create consistent loading states with skeleton components:
// Skeleton components for loading states
export function ProductCardSkeleton() {
return (
<Card>
<CardHeader>
<div className="h-4 w-32 bg-muted animate-pulse rounded" />
<div className="h-3 w-24 bg-muted animate-pulse rounded" />
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="h-3 w-full bg-muted animate-pulse rounded" />
<div className="h-3 w-4/5 bg-muted animate-pulse rounded" />
<div className="h-3 w-3/5 bg-muted animate-pulse rounded" />
</div>
</CardContent>
</Card>
)
}
export function TableSkeleton({ rows = 5 }: { rows?: number }) {
return (
<div className="space-y-4">
<div className="h-10 w-64 bg-muted animate-pulse rounded" />
<div className="border rounded-lg">
<div className="h-12 border-b bg-muted/50" />
{Array.from({ length: rows }).map((_, i) => (
<div key={i} className="h-16 border-b last:border-b-0 bg-muted/20 animate-pulse" />
))}
</div>
</div>
)
}
// Usage in components
export default function ProductsPage() {
return (
<Suspense fallback={<TableSkeleton />}>
<ProductTable />
</Suspense>
)
}
Error Handling in Components
Handle errors gracefully within components:
'use client'
import { useState, useEffect } from 'react'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { AlertCircle, RefreshCw } from 'lucide-react'
interface ProductListProps {
initialProducts?: Product[]
}
export function ProductList({ initialProducts = [] }: ProductListProps) {
const [products, setProducts] = useState(initialProducts)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const fetchProducts = async () => {
try {
setLoading(true)
setError(null)
const response = await fetch('/api/products')
if (!response.ok) {
throw new Error(`Failed to fetch products: ${response.statusText}`)
}
const data = await response.json()
setProducts(data.products)
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load products')
console.error('Error fetching products:', err)
} finally {
setLoading(false)
}
}
const handleRetry = () => {
fetchProducts()
}
if (error) {
return (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription className="flex items-center justify-between">
<span>{error}</span>
<Button
variant="outline"
size="sm"
onClick={handleRetry}
disabled={loading}
>
{loading ? (
<RefreshCw className="h-4 w-4 animate-spin" />
) : (
'Retry'
)}
</Button>
</AlertDescription>
</Alert>
)
}
if (loading && products.length === 0) {
return <ProductListSkeleton />
}
return (
<div className="space-y-4">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
{products.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
No products found
</div>
)}
</div>
)
}
Form Components
Build robust form components with validation:
'use client'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
const productSchema = z.object({
name: z.string().min(1, 'Product name is required'),
sku: z.string().min(1, 'SKU is required'),
price: z.number().positive('Price must be positive'),
description: z.string().optional(),
})
type ProductFormData = z.infer<typeof productSchema>
interface ProductFormProps {
initialData?: Partial<ProductFormData>
onSubmit: (data: ProductFormData) => Promise<void>
loading?: boolean
}
export function ProductForm({ initialData, onSubmit, loading = false }: ProductFormProps) {
const form = useForm<ProductFormData>({
resolver: zodResolver(productSchema),
defaultValues: {
name: initialData?.name || '',
sku: initialData?.sku || '',
price: initialData?.price || 0,
description: initialData?.description || '',
},
})
const handleSubmit = async (data: ProductFormData) => {
try {
await onSubmit(data)
form.reset()
} catch (error) {
console.error('Form submission error:', error)
}
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Product Name</FormLabel>
<FormControl>
<Input placeholder="Enter product name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="sku"
render={({ field }) => (
<FormItem>
<FormLabel>SKU</FormLabel>
<FormControl>
<Input placeholder="Enter SKU" {...field} />
</FormControl>
<FormDescription>
Unique product identifier
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="price"
render={({ field }) => (
<FormItem>
<FormLabel>Price</FormLabel>
<FormControl>
<Input
type="number"
step="0.01"
placeholder="0.00"
{...field}
onChange={(e) => field.onChange(parseFloat(e.target.value))}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder="Enter product description"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={loading}>
{loading ? 'Saving...' : 'Save Product'}
</Button>
</form>
</Form>
)
}
Performance Optimization
Use React optimization techniques:
import { memo, useMemo, useCallback } from 'react'
// Memoize expensive components
const ProductCard = memo(({ product, onEdit }: ProductCardProps) => {
return (
<Card>
<CardContent>
<h3>{product.name}</h3>
<p>{product.sku}</p>
<Button onClick={() => onEdit(product.id)}>Edit</Button>
</CardContent>
</Card>
)
})
// Memoize expensive calculations
function ProductList({ products, filters }: ProductListProps) {
const filteredProducts = useMemo(() => {
return products.filter(product =>
filters.every(filter => filter.predicate(product))
)
}, [products, filters])
const handleEdit = useCallback((productId: string) => {
// Edit logic
}, [])
return (
<div>
{filteredProducts.map(product => (
<ProductCard
key={product.id}
product={product}
onEdit={handleEdit}
/>
))}
</div>
)
}
Build maintainable, performant React components following these established patterns and best practices.