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.