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..." />
)
}
Centralize Related State
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
Group Related Components
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
- Explore advanced patterns for complex scenarios
- Learn about integration patterns with primitives
- Review component reference for detailed usage examples