Best Practices
Best practices for using FoundryKit utilities effectively
Best Practices
Learn the best practices for using FoundryKit utilities to write maintainable, performant, and reliable code.
Import Strategy
Optimize Bundle Size
Import only the utilities you need to minimize bundle size.
// ✅ Good: Named imports for tree shaking
import { cn, debounce, formatDate } from '@foundrykit/utils'
// ✅ Good: Import from specific modules
import { cn } from '@foundrykit/utils/cn'
import { debounce } from '@foundrykit/utils/function'
import { formatDate } from '@foundrykit/utils/date'
// ❌ Avoid: Importing entire package
import * as utils from '@foundrykit/utils'
const className = utils.cn('class1', 'class2')
// ❌ Avoid: Default imports when named imports exist
import cn from '@foundrykit/utils/cn' // May not work as expected
Consistent Import Patterns
Establish consistent import patterns across your project.
// Establish a consistent pattern for your team
import {
cn, // Class name utility
debounce, // Function utilities
throttle,
formatDate, // Date utilities
parseDate,
pick, // Object utilities
omit,
sortBy, // Array utilities
groupBy
} from '@foundrykit/utils'
// Group imports by category for readability
import {
// Class utilities
cn, clsx,
// Function utilities
debounce, throttle, memoize,
// Data utilities
pick, omit, sortBy, groupBy
} from '@foundrykit/utils'
Function Usage Best Practices
Debouncing and Throttling
Use debouncing and throttling appropriately for different scenarios.
import { debounce, throttle } from '@foundrykit/utils'
// ✅ Good: Debounce for search inputs (wait for user to stop typing)
const debouncedSearch = debounce((query: string) => {
performSearch(query)
}, 300)
// ✅ Good: Throttle for scroll events (limit frequency)
const throttledScroll = throttle(() => {
updateScrollPosition()
}, 100)
// ✅ Good: Debounce for form validation (wait for user to finish)
const debouncedValidation = debounce((formData: FormData) => {
validateForm(formData)
}, 500)
// ❌ Avoid: Wrong tool for the job
const badScrollDebounce = debounce(() => {
updateScrollPosition() // Should be throttled, not debounced
}, 100)
const badSearchThrottle = throttle((query: string) => {
performSearch(query) // Should be debounced, not throttled
}, 300)
Memoization Strategy
Use memoization strategically for expensive computations.
import { memoize } from '@foundrykit/utils'
// ✅ Good: Memoize pure functions with stable inputs
const calculateComplexValue = memoize((data: number[]) => {
return data.reduce((acc, val) => acc + Math.pow(val, 2), 0)
})
// ✅ Good: Memoize with custom key resolver for objects
const processUserData = memoize(
(users: User[]) => {
return users.map(user => ({
...user,
fullName: `${user.firstName} ${user.lastName}`
}))
},
{
resolver: (users: User[]) => {
// Create stable key based on user IDs and a version
return `${users.map(u => u.id).join(',')}-${users.length}`
}
}
)
// ❌ Avoid: Memoizing functions with constantly changing inputs
const badMemoization = memoize((timestamp: number, data: any) => {
// New cache entry for every timestamp - memory leak!
return processData(data)
})
// ✅ Better: Extract stable parts
const goodMemoization = memoize((data: any) => {
return processData(data)
})
// Use it with timestamp separately
const result = goodMemoization(data)
logWithTimestamp(result, Date.now())
Data Processing Best Practices
Efficient Array Operations
Chain array operations efficiently and choose the right utility for the job.
import { sortBy, groupBy, unique, chunk } from '@foundrykit/utils'
interface Product {
id: string
name: string
category: string
price: number
tags: string[]
}
// ✅ Good: Chain operations efficiently
function processProducts(products: Product[]) {
// Sort once, use multiple times
const sortedProducts = sortBy(products, 'price')
// Group the already sorted data
const groupedByCategory = groupBy(sortedProducts, 'category')
// Get unique categories from the grouped keys (more efficient)
const categories = Object.keys(groupedByCategory)
return {
sortedProducts,
groupedByCategory,
categories
}
}
// ❌ Avoid: Multiple iterations over the same data
function inefficientProcessing(products: Product[]) {
const sorted = sortBy(products, 'price') // Iteration 1
const grouped = groupBy(products, 'category') // Iteration 2 (on original data)
const categories = unique(products.map(p => p.category)) // Iteration 3
const expensive = products.filter(p => p.price > 100) // Iteration 4
return { sorted, grouped, categories, expensive }
}
// ✅ Better: Single iteration with reduce
function efficientProcessing(products: Product[]) {
const result = products.reduce((acc, product) => {
// Collect expensive products
if (product.price > 100) {
acc.expensive.push(product)
}
// Collect unique categories
if (!acc.categories.includes(product.category)) {
acc.categories.push(product.category)
}
return acc
}, {
expensive: [] as Product[],
categories: [] as string[]
})
// Then sort and group as needed
const sorted = sortBy(products, 'price')
const grouped = groupBy(sorted, 'category')
return { ...result, sorted, grouped }
}
Object Manipulation
Use object utilities consistently and safely.
import { pick, omit, deepMerge, isEqual } from '@foundrykit/utils'
interface User {
id: string
name: string
email: string
password: string
profile: {
avatar: string
preferences: {
theme: string
notifications: boolean
}
}
}
// ✅ Good: Consistent data transformation
class UserService {
// Always use the same pattern for public user data
static toPublicUser(user: User) {
return pick(user, ['id', 'name', 'email', 'profile'])
}
// Consistent pattern for safe updates
static updateUserPreferences(user: User, newPreferences: any) {
return deepMerge(user, {
profile: {
preferences: newPreferences
}
})
}
// Consistent comparison for change detection
static hasUserChanged(oldUser: User, newUser: User) {
const oldPublic = this.toPublicUser(oldUser)
const newPublic = this.toPublicUser(newUser)
return !isEqual(oldPublic, newPublic)
}
}
// ❌ Avoid: Inconsistent patterns
function inconsistentUserHandling(user: User) {
// Sometimes using pick
const publicData1 = pick(user, ['id', 'name'])
// Sometimes manual destructuring
const { password, ...publicData2 } = user
// Sometimes omit
const publicData3 = omit(user, ['password'])
// Inconsistent - hard to maintain
return { publicData1, publicData2, publicData3 }
}
Component Integration
React Component Patterns
Integrate utilities effectively with React components.
import React, { useState, useMemo, useCallback, useEffect } from 'react'
import { cn, debounce, sortBy, groupBy } from '@foundrykit/utils'
interface DataTableProps {
data: any[]
className?: string
sortable?: boolean
groupable?: boolean
}
// ✅ Good: Proper utility integration with React
export function DataTable({
data,
className,
sortable = true,
groupable = false
}: DataTableProps) {
const [sortField, setSortField] = useState<string>('')
const [groupField, setGroupField] = useState<string>('')
const [searchQuery, setSearchQuery] = useState('')
const [debouncedQuery, setDebouncedQuery] = useState('')
// ✅ Good: Create debounced function with useMemo
const debouncedSearch = useMemo(
() => debounce((query: string) => {
setDebouncedQuery(query)
}, 300),
[]
)
// ✅ Good: Cleanup debounced function
useEffect(() => {
return () => {
debouncedSearch.cancel()
}
}, [debouncedSearch])
// ✅ Good: Memoize expensive computations
const processedData = useMemo(() => {
let result = data
// Filter by search query
if (debouncedQuery) {
result = result.filter(item =>
Object.values(item).some(value =>
String(value).toLowerCase().includes(debouncedQuery.toLowerCase())
)
)
}
// Sort if needed
if (sortField && sortable) {
result = sortBy(result, sortField)
}
return result
}, [data, debouncedQuery, sortField, sortable])
// ✅ Good: Memoize grouped data separately
const groupedData = useMemo(() => {
if (!groupable || !groupField) return null
return groupBy(processedData, groupField)
}, [processedData, groupField, groupable])
// ✅ Good: Stable callback functions
const handleSearchChange = useCallback((value: string) => {
setSearchQuery(value)
debouncedSearch(value)
}, [debouncedSearch])
const handleSortChange = useCallback((field: string) => {
setSortField(field)
}, [])
return (
<div className={cn('data-table', className)}>
<div className="data-table-controls">
<input
value={searchQuery}
onChange={(e) => handleSearchChange(e.target.value)}
placeholder="Search..."
className="search-input"
/>
{sortable && (
<select
value={sortField}
onChange={(e) => handleSortChange(e.target.value)}
className="sort-select"
>
<option value="">No sorting</option>
<option value="name">Sort by Name</option>
<option value="date">Sort by Date</option>
</select>
)}
</div>
<div className="data-table-content">
{groupedData ? (
Object.entries(groupedData).map(([group, items]) => (
<div key={group} className="data-group">
<h3 className="group-title">{group}</h3>
{items.map((item, index) => (
<div key={index} className="data-item">
{/* Render item */}
</div>
))}
</div>
))
) : (
processedData.map((item, index) => (
<div key={index} className="data-item">
{/* Render item */}
</div>
))
)}
</div>
</div>
)
}
Class Name Management
Use cn
effectively for dynamic styling.
import { cn } from '@foundrykit/utils'
// ✅ Good: Consistent class name patterns
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
loading?: boolean
className?: string
}
export function Button({
variant = 'primary',
size = 'md',
disabled = false,
loading = false,
className,
...props
}: ButtonProps) {
return (
<button
className={cn(
// Base styles - always applied
'inline-flex items-center justify-center rounded-md font-medium transition-colors',
'focus-visible:outline-none focus-visible:ring-2',
// Size variants - organized by property
{
'px-2 py-1 text-xs': size === 'sm',
'px-4 py-2 text-sm': size === 'md',
'px-6 py-3 text-base': size === 'lg',
},
// Color variants - organized by property
{
'bg-blue-600 text-white hover:bg-blue-700': variant === 'primary',
'bg-gray-200 text-gray-900 hover:bg-gray-300': variant === 'secondary',
'bg-red-600 text-white hover:bg-red-700': variant === 'danger',
},
// State modifiers - organized by state
{
'opacity-50 cursor-not-allowed': disabled || loading,
'animate-pulse': loading,
},
// External className override - always last
className
)}
disabled={disabled || loading}
{...props}
/>
)
}
// ❌ Avoid: Inconsistent or complex class logic
function BadButton({ variant, size, disabled, className, ...props }) {
// Hard to read and maintain
const buttonClass = `
${variant === 'primary' ? 'bg-blue-600 text-white' : ''}
${variant === 'secondary' ? 'bg-gray-200 text-gray-900' : ''}
${size === 'sm' ? 'px-2 py-1 text-xs' : size === 'lg' ? 'px-6 py-3 text-base' : 'px-4 py-2 text-sm'}
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
${className || ''}
`.trim().replace(/\s+/g, ' ')
return <button className={buttonClass} {...props} />
}
Error Handling
Validation Best Practices
Use validation utilities consistently and provide good error messages.
import { isEmail, validateSchema, isUrl } from '@foundrykit/utils'
// ✅ Good: Comprehensive validation schema
const userSchema = {
name: {
type: 'string',
required: true,
minLength: 2,
maxLength: 50,
errorMessage: 'Name must be between 2 and 50 characters'
},
email: {
type: 'string',
required: true,
validator: isEmail,
errorMessage: 'Please enter a valid email address'
},
website: {
type: 'string',
required: false,
validator: isUrl,
errorMessage: 'Please enter a valid URL'
},
age: {
type: 'number',
required: true,
min: 13,
max: 120,
errorMessage: 'Age must be between 13 and 120'
}
}
// ✅ Good: Validation service with consistent error handling
class ValidationService {
static validateUser(userData: any) {
const result = validateSchema(userData, userSchema)
return {
isValid: result.isValid,
errors: this.formatErrors(result.errors),
data: result.isValid ? userData : null
}
}
private static formatErrors(errors: Record<string, string>) {
const formatted: Record<string, string> = {}
Object.entries(errors).forEach(([field, error]) => {
// Use custom error messages from schema if available
const schemaField = userSchema[field as keyof typeof userSchema]
formatted[field] = schemaField?.errorMessage || error
})
return formatted
}
// Validate individual fields for real-time feedback
static validateField(field: string, value: any) {
const fieldSchema = userSchema[field as keyof typeof userSchema]
if (!fieldSchema) return { isValid: true, error: null }
const result = validateSchema({ [field]: value }, { [field]: fieldSchema })
return {
isValid: result.isValid,
error: result.isValid ? null : (fieldSchema.errorMessage || result.errors[field])
}
}
}
// Usage in component
export function UserForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
website: '',
age: 0
})
const [errors, setErrors] = useState<Record<string, string>>({})
const handleFieldChange = (field: string, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }))
// Real-time validation
const fieldValidation = ValidationService.validateField(field, value)
setErrors(prev => ({
...prev,
[field]: fieldValidation.error || ''
}))
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
const validation = ValidationService.validateUser(formData)
if (validation.isValid) {
// Submit form
submitUser(validation.data)
} else {
setErrors(validation.errors)
}
}
return (
<form onSubmit={handleSubmit}>
{/* Form fields with error handling */}
</form>
)
}
Testing Best Practices
Testing Utility Functions
Write comprehensive tests for utility usage.
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { debounce, memoize, sortBy, groupBy } from '@foundrykit/utils'
describe('Utility Integration Tests', () => {
describe('debounce', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.restoreAllTimers()
})
it('should debounce function calls', () => {
const mockFn = vi.fn()
const debouncedFn = debounce(mockFn, 300)
debouncedFn('arg1')
debouncedFn('arg2')
debouncedFn('arg3')
expect(mockFn).not.toHaveBeenCalled()
vi.advanceTimersByTime(300)
expect(mockFn).toHaveBeenCalledTimes(1)
expect(mockFn).toHaveBeenCalledWith('arg3')
})
it('should cancel debounced calls', () => {
const mockFn = vi.fn()
const debouncedFn = debounce(mockFn, 300)
debouncedFn('arg1')
debouncedFn.cancel()
vi.advanceTimersByTime(300)
expect(mockFn).not.toHaveBeenCalled()
})
})
describe('memoize', () => {
it('should cache function results', () => {
const expensiveFn = vi.fn((x: number) => x * 2)
const memoizedFn = memoize(expensiveFn)
const result1 = memoizedFn(5)
const result2 = memoizedFn(5)
expect(result1).toBe(10)
expect(result2).toBe(10)
expect(expensiveFn).toHaveBeenCalledTimes(1)
})
it('should clear cache when requested', () => {
const expensiveFn = vi.fn((x: number) => x * 2)
const memoizedFn = memoize(expensiveFn)
memoizedFn(5)
memoizedFn.cache.clear()
memoizedFn(5)
expect(expensiveFn).toHaveBeenCalledTimes(2)
})
})
describe('data processing', () => {
const testData = [
{ id: 1, name: 'Alice', category: 'A', value: 100 },
{ id: 2, name: 'Bob', category: 'B', value: 200 },
{ id: 3, name: 'Charlie', category: 'A', value: 150 }
]
it('should sort data correctly', () => {
const sorted = sortBy(testData, 'value')
expect(sorted[0].name).toBe('Alice')
expect(sorted[1].name).toBe('Charlie')
expect(sorted[2].name).toBe('Bob')
})
it('should group data correctly', () => {
const grouped = groupBy(testData, 'category')
expect(grouped.A).toHaveLength(2)
expect(grouped.B).toHaveLength(1)
expect(grouped.A[0].name).toBe('Alice')
expect(grouped.A[1].name).toBe('Charlie')
})
})
})
// Test React component integration
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { DataTable } from './DataTable' // Component using utilities
describe('DataTable Component', () => {
const mockData = [
{ id: 1, name: 'Item 1', category: 'A' },
{ id: 2, name: 'Item 2', category: 'B' },
{ id: 3, name: 'Item 3', category: 'A' }
]
it('should filter data when searching', async () => {
render(<DataTable data={mockData} />)
const searchInput = screen.getByPlaceholderText('Search...')
fireEvent.change(searchInput, { target: { value: 'Item 1' } })
// Wait for debounced search
await waitFor(() => {
expect(screen.getByText('Item 1')).toBeInTheDocument()
expect(screen.queryByText('Item 2')).not.toBeInTheDocument()
}, { timeout: 500 })
})
it('should sort data when sort field changes', () => {
render(<DataTable data={mockData} sortable />)
const sortSelect = screen.getByDisplayValue('No sorting')
fireEvent.change(sortSelect, { target: { value: 'name' } })
const items = screen.getAllByText(/Item \d/)
expect(items[0]).toHaveTextContent('Item 1')
expect(items[1]).toHaveTextContent('Item 2')
expect(items[2]).toHaveTextContent('Item 3')
})
})
Performance Best Practices
Optimization Guidelines
Follow these guidelines for optimal performance.
// ✅ Good: Efficient utility usage patterns
class OptimizedDataProcessor {
private memoizedSort = memoize((data: any[], field: string) => {
return sortBy(data, field)
})
private memoizedGroup = memoize((data: any[], field: string) => {
return groupBy(data, field)
})
processData(data: any[], sortField: string, groupField?: string) {
// Sort first (memoized)
const sortedData = this.memoizedSort(data, sortField)
// Group if needed (memoized)
const groupedData = groupField
? this.memoizedGroup(sortedData, groupField)
: null
return { sortedData, groupedData }
}
// Clear caches when data structure changes
clearCaches() {
this.memoizedSort.cache.clear()
this.memoizedGroup.cache.clear()
}
}
// ❌ Avoid: Inefficient patterns
class InefficientProcessor {
processData(data: any[], sortField: string, groupField?: string) {
// Creates new memoized functions every time - no caching benefit!
const sort = memoize((data: any[], field: string) => sortBy(data, field))
const group = memoize((data: any[], field: string) => groupBy(data, field))
const sortedData = sort(data, sortField)
const groupedData = groupField ? group(data, groupField) : null
return { sortedData, groupedData }
}
}
Code Organization
Module Structure
Organize utility usage consistently across your project.
// utils/dataProcessing.ts - Centralized data processing utilities
import { sortBy, groupBy, unique, chunk } from '@foundrykit/utils'
export class DataProcessor {
static sortAndGroup<T>(
data: T[],
sortField: keyof T,
groupField: keyof T
) {
const sorted = sortBy(data, sortField as string)
const grouped = groupBy(sorted, groupField as string)
return { sorted, grouped }
}
static processInChunks<T>(
data: T[],
processor: (chunk: T[]) => T[],
chunkSize: number = 1000
) {
const chunks = chunk(data, chunkSize)
return chunks.flatMap(processor)
}
}
// utils/validation.ts - Centralized validation
import { isEmail, isUrl, validateSchema } from '@foundrykit/utils'
export const commonValidators = {
email: isEmail,
url: isUrl,
required: (value: any) => value != null && value !== '',
minLength: (min: number) => (value: string) => value.length >= min
}
export const validateUser = (userData: any) => {
return validateSchema(userData, {
email: { type: 'string', required: true, validator: isEmail },
name: { type: 'string', required: true, minLength: 2 }
})
}
// utils/ui.ts - UI-related utilities
import { cn, debounce, throttle } from '@foundrykit/utils'
export const uiHelpers = {
cn,
createDebouncedSearch: (callback: (query: string) => void, delay = 300) => {
return debounce(callback, delay)
},
createThrottledScroll: (callback: () => void, interval = 100) => {
return throttle(callback, interval)
}
}
Common Anti-Patterns
What to Avoid
// ❌ Don't create utility instances in render loops
function BadComponent({ items }) {
return (
<div>
{items.map(item => {
// Creates new debounced function for each item!
const debouncedFn = debounce(() => {}, 300)
return <div key={item.id}>{item.name}</div>
})}
</div>
)
}
// ❌ Don't ignore cleanup
function BadHook() {
const debouncedFn = debounce(() => {}, 300)
// Missing cleanup - potential memory leak
return { debouncedFn }
}
// ❌ Don't use wrong utility for the job
function badEventHandling() {
// Should be throttled, not debounced
const badScrollHandler = debounce(() => {
updateScrollPosition()
}, 100)
// Should be debounced, not throttled
const badSearchHandler = throttle((query: string) => {
performSearch(query)
}, 300)
}
// ❌ Don't ignore memoization cache management
function BadMemoization() {
const expensiveFn = memoize((data: any[]) => {
return processLargeDataset(data)
})
// Cache grows indefinitely - memory leak!
// Should clear cache when appropriate
}
Best Practices Checklist
Development Checklist
- Use named imports for tree shaking
- Create utility instances outside render loops
- Clean up debounced/throttled functions
- Clear memoization caches when data changes
- Use appropriate utility for each use case
- Provide meaningful error messages
- Write tests for utility integration
- Monitor bundle size impact
- Organize utilities in logical modules
- Document complex utility usage
Code Review Checklist
- Are utilities imported efficiently?
- Are debounced/throttled functions cleaned up?
- Is memoization used appropriately?
- Are the right utilities used for each task?
- Is error handling comprehensive?
- Are performance implications considered?
- Is the code testable and tested?
- Are patterns consistent across the codebase?
Next Steps
- Review performance tips for optimization techniques
- Explore the utility functions reference for detailed API documentation
- Learn about common patterns for effective usage