FoundryKit

Best Practices

Best practices for using FoundryKit components effectively

Best Practices

Follow these best practices to get the most out of FoundryKit components and build maintainable, performant applications.

Component Usage

Prefer Composition Over Custom Implementation

Use existing components rather than building custom solutions:

// ✅ Good - Use existing components
import { SearchBar, Calendar, Pagination } from '@foundrykit/components'

function DataInterface() {
  return (
    <div className="space-y-4">
      <SearchBar placeholder="Search..." onSearch={handleSearch} />
      <Calendar mode="single" selected={date} onSelect={setDate} />
      <Pagination currentPage={page} totalPages={total} onPageChange={setPage} />
    </div>
  )
}

// ❌ Avoid - Custom implementation
function CustomDataInterface() {
  return (
    <div className="space-y-4">
      <input 
        type="text" 
        placeholder="Search..." 
        onChange={handleSearchChange}
        className="border rounded px-3 py-2"
      />
      {/* Custom calendar implementation */}
      {/* Custom pagination implementation */}
    </div>
  )
}

Use Proper TypeScript Types

Leverage TypeScript for better type safety:

// ✅ Good - Proper typing
import { Calendar, CalendarProps } from '@foundrykit/components'

interface MyCalendarProps extends CalendarProps {
  customLabel?: string
}

function MyCalendar({ customLabel, ...props }: MyCalendarProps) {
  return (
    <div>
      {customLabel && <label>{customLabel}</label>}
      <Calendar {...props} />
    </div>
  )
}

// ❌ Avoid - Any types
function MyCalendar(props: any) {
  return <Calendar {...props} />
}

Performance Optimization

Memoize Expensive Operations

Use memoization for expensive computations:

import { useMemo, useCallback } from 'react'
import { SearchBar, Pagination } from '@foundrykit/components'

function OptimizedDataList({ items, searchQuery, currentPage }) {
  // Memoize filtered results
  const filteredItems = useMemo(() => {
    return items.filter(item =>
      item.name.toLowerCase().includes(searchQuery.toLowerCase())
    )
  }, [items, searchQuery])

  // Memoize paginated results
  const paginatedItems = useMemo(() => {
    const startIndex = (currentPage - 1) * 10
    return filteredItems.slice(startIndex, startIndex + 10)
  }, [filteredItems, currentPage])

  // Memoize callback functions
  const handleSearch = useCallback((query: string) => {
    setSearchQuery(query)
    setCurrentPage(1) // Reset to first page
  }, [])

  const handlePageChange = useCallback((page: number) => {
    setCurrentPage(page)
  }, [])

  return (
    <div className="space-y-4">
      <SearchBar onSearch={handleSearch} />
      <div>
        {paginatedItems.map(item => (
          <div key={item.id}>{item.name}</div>
        ))}
      </div>
      <Pagination
        currentPage={currentPage}
        totalPages={Math.ceil(filteredItems.length / 10)}
        onPageChange={handlePageChange}
      />
    </div>
  )
}

Lazy Load Components

Lazy load components that aren't immediately needed:

import { lazy, Suspense } from 'react'
import { SearchBar } from '@foundrykit/components'
import { Skeleton } from '@foundrykit/primitives'

const Calendar = lazy(() => import('@foundrykit/components').then(m => ({ default: m.Calendar })))
const Command = lazy(() => import('@foundrykit/components').then(m => ({ default: m.Command })))

function LazyLoadedInterface() {
  const [showCalendar, setShowCalendar] = useState(false)
  const [showCommand, setShowCommand] = useState(false)

  return (
    <div className="space-y-4">
      <SearchBar placeholder="Search..." onSearch={console.log} />
      
      <button onClick={() => setShowCalendar(true)}>
        Show Calendar
      </button>
      
      {showCalendar && (
        <Suspense fallback={<Skeleton className="h-32 w-full" />}>
          <Calendar mode="single" />
        </Suspense>
      )}
      
      <button onClick={() => setShowCommand(true)}>
        Show Commands
      </button>
      
      {showCommand && (
        <Suspense fallback={<Skeleton className="h-20 w-full" />}>
          <Command>
            <CommandInput placeholder="Loading..." />
          </Command>
        </Suspense>
      )}
    </div>
  )
}

State Management

Use Controlled Components

Prefer controlled components for predictable state management:

// ✅ Good - Controlled components
function ControlledSearch() {
  const [searchQuery, setSearchQuery] = useState('')
  const [selectedDate, setSelectedDate] = useState<Date>()

  const handleSearch = useCallback((query: string) => {
    setSearchQuery(query)
    // Perform search logic
  }, [])

  const handleDateSelect = useCallback((date: Date | undefined) => {
    setSelectedDate(date)
    // Handle date selection
  }, [])

  return (
    <div className="space-y-4">
      <SearchBar
        placeholder="Search..."
        onSearch={handleSearch}
      />
      <Calendar
        mode="single"
        selected={selectedDate}
        onSelect={handleDateSelect}
      />
    </div>
  )
}

// ❌ Avoid - Uncontrolled components with refs
function UncontrolledSearch() {
  const searchRef = useRef<HTMLInputElement>(null)
  
  const handleSubmit = () => {
    const query = searchRef.current?.value
    // Handle search
  }

  return (
    <input ref={searchRef} placeholder="Search..." />
  )
}

Group related state together:

// ✅ Good - Centralized state
interface SearchState {
  query: string
  date: Date | undefined
  page: number
  filters: Record<string, any>
}

function SearchInterface() {
  const [searchState, setSearchState] = useState<SearchState>({
    query: '',
    date: undefined,
    page: 1,
    filters: {}
  })

  const updateSearchState = useCallback((updates: Partial<SearchState>) => {
    setSearchState(prev => ({ ...prev, ...updates }))
  }, [])

  return (
    <div className="space-y-4">
      <SearchBar
        placeholder="Search..."
        onSearch={(query) => updateSearchState({ query, page: 1 })}
      />
      <Calendar
        mode="single"
        selected={searchState.date}
        onSelect={(date) => updateSearchState({ date, page: 1 })}
      />
      <Pagination
        currentPage={searchState.page}
        totalPages={10}
        onPageChange={(page) => updateSearchState({ page })}
      />
    </div>
  )
}

// ❌ Avoid - Scattered state
function ScatteredSearch() {
  const [query, setQuery] = useState('')
  const [date, setDate] = useState<Date>()
  const [page, setPage] = useState(1)
  const [filters, setFilters] = useState({})
  // More scattered state...
}

Error Handling

Handle Component Errors Gracefully

Implement proper error handling for components:

import { useState, useCallback } from 'react'
import { SearchBar, Calendar } from '@foundrykit/components'
import { Card, CardContent, Button } from '@foundrykit/primitives'

function SafeSearchInterface() {
  const [error, setError] = useState<string | null>(null)
  const [isLoading, setIsLoading] = useState(false)

  const handleSearch = useCallback(async (query: string) => {
    try {
      setIsLoading(true)
      setError(null)
      
      // Perform search
      await performSearch(query)
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Search failed')
    } finally {
      setIsLoading(false)
    }
  }, [])

  const handleDateSelect = useCallback(async (date: Date | undefined) => {
    try {
      setIsLoading(true)
      setError(null)
      
      // Handle date selection
      await handleDateChange(date)
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Date selection failed')
    } finally {
      setIsLoading(false)
    }
  }, [])

  if (error) {
    return (
      <Card>
        <CardContent className="text-center space-y-4">
          <p className="text-destructive">{error}</p>
          <Button onClick={() => setError(null)}>Try Again</Button>
        </CardContent>
      </Card>
    )
  }

  return (
    <div className="space-y-4">
      <SearchBar
        placeholder="Search..."
        onSearch={handleSearch}
        disabled={isLoading}
      />
      <Calendar
        mode="single"
        onSelect={handleDateSelect}
        disabled={isLoading}
      />
      {isLoading && (
        <div className="text-sm text-muted-foreground">Loading...</div>
      )}
    </div>
  )
}

Validate Props

Validate component props to prevent runtime errors:

import { SearchBar, Calendar, Pagination } from '@foundrykit/components'

interface DataInterfaceProps {
  items: any[]
  onSearch: (query: string) => void
  onDateSelect: (date: Date | undefined) => void
  onPageChange: (page: number) => void
  currentPage: number
  totalPages: number
}

function DataInterface({ 
  items, 
  onSearch, 
  onDateSelect, 
  onPageChange, 
  currentPage, 
  totalPages 
}: DataInterfaceProps) {
  // Validate props
  if (!Array.isArray(items)) {
    throw new Error('items must be an array')
  }

  if (typeof onSearch !== 'function') {
    throw new Error('onSearch must be a function')
  }

  if (currentPage < 1 || currentPage > totalPages) {
    throw new Error('currentPage must be between 1 and totalPages')
  }

  return (
    <div className="space-y-4">
      <SearchBar onSearch={onSearch} />
      <Calendar mode="single" onSelect={onDateSelect} />
      <Pagination
        currentPage={currentPage}
        totalPages={totalPages}
        onPageChange={onPageChange}
      />
    </div>
  )
}

Accessibility

Provide Proper Labels

Always provide descriptive labels for components:

// ✅ Good - Proper labels
function AccessibleSearch() {
  return (
    <div className="space-y-4">
      <div>
        <label htmlFor="search" className="sr-only">Search products</label>
        <SearchBar
          id="search"
          placeholder="Search products..."
          onSearch={handleSearch}
        />
      </div>
      
      <div>
        <label className="sr-only">Select date</label>
        <Calendar
          mode="single"
          selected={date}
          onSelect={setDate}
          aria-label="Select date"
        />
      </div>
    </div>
  )
}

// ❌ Avoid - Missing labels
function InaccessibleSearch() {
  return (
    <div className="space-y-4">
      <SearchBar placeholder="Search..." onSearch={handleSearch} />
      <Calendar mode="single" selected={date} onSelect={setDate} />
    </div>
  )
}

Handle Keyboard Navigation

Ensure proper keyboard navigation:

import { useCallback } from 'react'
import { SearchBar, Calendar } from '@foundrykit/components'

function KeyboardAccessibleInterface() {
  const handleKeyDown = useCallback((event: KeyboardEvent) => {
    if (event.key === 'Escape') {
      // Close any open components
      setShowCalendar(false)
      setShowSearch(false)
    }
  }, [])

  return (
    <div onKeyDown={handleKeyDown}>
      <SearchBar
        placeholder="Search (Press Escape to clear)..."
        onSearch={handleSearch}
      />
      <Calendar
        mode="single"
        selected={date}
        onSelect={setDate}
      />
    </div>
  )
}

Styling

Use Consistent Theming

Maintain consistent theming across components:

// ✅ Good - Consistent theming
function ThemedInterface() {
  return (
    <div className="space-y-6">
      <SearchBar
        placeholder="Search..."
        onSearch={handleSearch}
        className="rounded-lg border border-border"
      />
      
      <Calendar
        mode="single"
        selected={date}
        onSelect={setDate}
        className="rounded-lg border border-border"
      />
      
      <Pagination
        currentPage={currentPage}
        totalPages={totalPages}
        onPageChange={setCurrentPage}
        className="rounded-lg"
      />
    </div>
  )
}

// ❌ Avoid - Inconsistent styling
function InconsistentInterface() {
  return (
    <div className="space-y-6">
      <SearchBar
        placeholder="Search..."
        onSearch={handleSearch}
        className="border-2 border-blue-500 rounded-md"
      />
      
      <Calendar
        mode="single"
        selected={date}
        onSelect={setDate}
        className="border border-gray-300 rounded-lg"
      />
      
      <Pagination
        currentPage={currentPage}
        totalPages={totalPages}
        onPageChange={setCurrentPage}
        className="rounded"
      />
    </div>
  )
}

Use CSS Variables for Theming

Leverage CSS variables for consistent theming:

:root {
  /* Component-specific variables */
  --search-border-radius: var(--radius);
  --search-border-color: var(--border);
  --search-background: var(--background);
  
  --calendar-border-radius: var(--radius);
  --calendar-border-color: var(--border);
  
  --pagination-border-radius: var(--radius);
  --pagination-active-color: var(--primary);
}

Testing

Test Component Behavior

Write tests that focus on component behavior:

import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { SearchBar, Calendar, Pagination } from '@foundrykit/components'

describe('SearchBar', () => {
  it('calls onSearch with debounced value', async () => {
    const onSearch = jest.fn()
    render(<SearchBar onSearch={onSearch} />)
    
    const input = screen.getByPlaceholderText(/search/i)
    fireEvent.change(input, { target: { value: 'test query' } })
    
    await waitFor(() => {
      expect(onSearch).toHaveBeenCalledWith('test query')
    }, { timeout: 400 })
  })
})

describe('Calendar', () => {
  it('calls onSelect when date is clicked', () => {
    const onSelect = jest.fn()
    render(<Calendar mode="single" onSelect={onSelect} />)
    
    const todayButton = screen.getByRole('button', { name: /today/i })
    fireEvent.click(todayButton)
    
    expect(onSelect).toHaveBeenCalled()
  })
})

describe('Pagination', () => {
  it('calls onPageChange when page is clicked', () => {
    const onPageChange = jest.fn()
    render(
      <Pagination
        currentPage={1}
        totalPages={5}
        onPageChange={onPageChange}
      />
    )
    
    const page2Button = screen.getByRole('button', { name: /2/i })
    fireEvent.click(page2Button)
    
    expect(onPageChange).toHaveBeenCalledWith(2)
  })
})

Test Accessibility

Ensure components are accessible:

import { render, screen } from '@testing-library/react'
import { SearchBar, Calendar } from '@foundrykit/components'

describe('Accessibility', () => {
  it('SearchBar has proper ARIA attributes', () => {
    render(<SearchBar placeholder="Search..." onSearch={jest.fn()} />)
    
    const input = screen.getByPlaceholderText(/search/i)
    expect(input).toHaveAttribute('role', 'searchbox')
  })
  
  it('Calendar has proper ARIA attributes', () => {
    render(<Calendar mode="single" onSelect={jest.fn()} />)
    
    const calendar = screen.getByRole('grid')
    expect(calendar).toHaveAttribute('aria-label', 'Calendar')
  })
})

Code Organization

Organize related components together:

// components/search/SearchInterface.tsx
export function SearchInterface() {
  return (
    <div className="space-y-4">
      <SearchBar placeholder="Search..." onSearch={handleSearch} />
      <Calendar mode="single" selected={date} onSelect={setDate} />
    </div>
  )
}

// components/search/SearchResults.tsx
export function SearchResults({ results }) {
  return (
    <div className="space-y-2">
      {results.map(result => (
        <div key={result.id}>{result.name}</div>
      ))}
    </div>
  )
}

// components/search/SearchPagination.tsx
export function SearchPagination({ currentPage, totalPages, onPageChange }) {
  return (
    <Pagination
      currentPage={currentPage}
      totalPages={totalPages}
      onPageChange={onPageChange}
    />
  )
}

Use Custom Hooks

Extract reusable logic into custom hooks:

// hooks/useSearch.ts
export function useSearch(initialQuery = '') {
  const [query, setQuery] = useState(initialQuery)
  const [results, setResults] = useState([])
  const [isLoading, setIsLoading] = useState(false)

  const search = useCallback(async (searchQuery: string) => {
    setIsLoading(true)
    try {
      const searchResults = await performSearch(searchQuery)
      setResults(searchResults)
    } catch (error) {
      console.error('Search failed:', error)
      setResults([])
    } finally {
      setIsLoading(false)
    }
  }, [])

  return {
    query,
    results,
    isLoading,
    search
  }
}

// Usage
function SearchComponent() {
  const { query, results, isLoading, search } = useSearch()

  return (
    <div className="space-y-4">
      <SearchBar onSearch={search} />
      {isLoading ? (
        <div>Loading...</div>
      ) : (
        <div>
          {results.map(result => (
            <div key={result.id}>{result.name}</div>
          ))}
        </div>
      )}
    </div>
  )
}

Common Anti-Patterns

Avoid Over-Engineering

Don't over-engineer simple use cases:

// ❌ Avoid - Over-engineered
function OverEngineeredSearch() {
  const [searchState, setSearchState] = useState({
    query: '',
    history: [],
    suggestions: [],
    filters: {},
    sortBy: 'name',
    sortOrder: 'asc'
  })

  const searchReducer = useReducer(searchReducerFunction, searchState)
  
  // Complex state management for simple search
  return <SearchBar onSearch={handleComplexSearch} />
}

// ✅ Good - Simple and focused
function SimpleSearch() {
  const [query, setQuery] = useState('')
  
  const handleSearch = useCallback((searchQuery: string) => {
    setQuery(searchQuery)
    // Simple search logic
  }, [])
  
  return <SearchBar onSearch={handleSearch} />
}

Avoid Prop Drilling

Use context or composition to avoid prop drilling:

// ❌ Avoid - Prop drilling
function ParentComponent() {
  const [searchQuery, setSearchQuery] = useState('')
  
  return (
    <ChildComponent
      searchQuery={searchQuery}
      setSearchQuery={setSearchQuery}
    />
  )
}

function ChildComponent({ searchQuery, setSearchQuery }) {
  return (
    <GrandChildComponent
      searchQuery={searchQuery}
      setSearchQuery={setSearchQuery}
    />
  )
}

// ✅ Good - Use context or composition
const SearchContext = createContext()

function SearchProvider({ children }) {
  const [searchQuery, setSearchQuery] = useState('')
  
  return (
    <SearchContext.Provider value={{ searchQuery, setSearchQuery }}>
      {children}
    </SearchContext.Provider>
  )
}

function ChildComponent() {
  const { searchQuery, setSearchQuery } = useContext(SearchContext)
  return <SearchBar onSearch={setSearchQuery} />
}

Next Steps