Best Practices
Best practices for using FoundryKit primitives effectively
Best Practices
Follow these best practices to get the most out of FoundryKit primitives and build maintainable, accessible, and performant applications.
Component Composition
Prefer Composition Over Inheritance
Build complex components by composing primitives together:
// ✅ Good - Composed from primitives
function UserCard({ user }) {
return (
<Card>
<CardHeader>
<div className="flex items-center space-x-4">
<Avatar>
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback>{user.initials}</AvatarFallback>
</Avatar>
<div>
<CardTitle>{user.name}</CardTitle>
<CardDescription>{user.role}</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
<p>{user.bio}</p>
<div className="flex space-x-2 mt-4">
<Button size="sm">View Profile</Button>
<Button variant="outline" size="sm">Message</Button>
</div>
</CardContent>
</Card>
)
}
// ❌ Avoid - Custom implementation
function UserCard({ user }) {
return (
<div className="border rounded-lg p-4">
{/* Custom styling and behavior */}
</div>
)
}
Use Semantic HTML
Always use semantic HTML elements and proper ARIA attributes:
// ✅ Good - Semantic form structure
function ContactForm() {
return (
<form onSubmit={handleSubmit} className="space-y-4">
<fieldset>
<legend className="text-lg font-semibold">Contact Information</legend>
<div className="space-y-2">
<Label htmlFor="name">Full Name</Label>
<Input id="name" name="name" required />
</div>
<div className="space-y-2">
<Label htmlFor="email">Email Address</Label>
<Input id="email" name="email" type="email" required />
</div>
</fieldset>
<Button type="submit">Send Message</Button>
</form>
)
}
State Management
Use Controlled Components
Prefer controlled components for form inputs:
// ✅ Good - Controlled component
function ControlledForm() {
const [formData, setFormData] = useState({
name: '',
email: ''
})
const handleChange = (e) => {
const { name, value } = e.target
setFormData(prev => ({ ...prev, [name]: value }))
}
return (
<form className="space-y-4">
<div>
<Label htmlFor="name">Name</Label>
<Input
id="name"
name="name"
value={formData.name}
onChange={handleChange}
/>
</div>
<div>
<Label htmlFor="email">Email</Label>
<Input
id="email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
/>
</div>
</form>
)
}
Handle Loading States
Provide clear feedback during async operations:
function AsyncButton({ onClick, children }) {
const [isLoading, setIsLoading] = useState(false)
const handleClick = async () => {
setIsLoading(true)
try {
await onClick()
} finally {
setIsLoading(false)
}
}
return (
<Button onClick={handleClick} disabled={isLoading}>
{isLoading ? (
<>
<span className="animate-spin mr-2">⏳</span>
Loading...
</>
) : (
children
)}
</Button>
)
}
Performance Optimization
Memoize Expensive Components
Use React.memo for components that receive stable props:
const ExpensiveComponent = React.memo(function ExpensiveComponent({ data }) {
// Expensive rendering logic
return (
<Card>
<CardContent>
{data.map(item => (
<div key={item.id}>{item.name}</div>
))}
</CardContent>
</Card>
)
})
Lazy Load Components
Lazy load components that aren't immediately needed:
import { lazy, Suspense } from 'react'
import { Skeleton } from '@foundrykit/primitives'
const HeavyComponent = lazy(() => import('./HeavyComponent'))
function App() {
return (
<div>
<h1>My App</h1>
<Suspense fallback={<Skeleton className="h-32 w-full" />}>
<HeavyComponent />
</Suspense>
</div>
)
}
Accessibility
Provide Clear Labels
Always provide descriptive labels for form controls:
// ✅ Good - Clear, descriptive labels
<div className="space-y-2">
<Label htmlFor="password">
Password <span className="text-muted-foreground">(minimum 8 characters)</span>
</Label>
<Input
id="password"
type="password"
aria-describedby="password-requirements"
/>
<p id="password-requirements" className="text-sm text-muted-foreground">
Must contain at least one uppercase letter, one lowercase letter, and one number.
</p>
</div>
Handle Errors Gracefully
Provide clear error messages with proper ARIA attributes:
function FormWithErrors({ errors, touched }) {
return (
<form className="space-y-4">
<div>
<Label htmlFor="email">Email Address</Label>
<Input
id="email"
type="email"
aria-invalid={errors.email && touched.email ? 'true' : 'false'}
aria-describedby={errors.email && touched.email ? 'email-error' : undefined}
/>
{errors.email && touched.email && (
<p id="email-error" className="text-sm text-destructive" role="alert">
{errors.email}
</p>
)}
</div>
</form>
)
}
Styling
Use Design Tokens
Prefer design tokens over hardcoded values:
// ✅ Good - Uses design tokens
<Card className="rounded-lg border border-border bg-card text-card-foreground">
<CardContent className="p-6">
<p className="text-card-foreground">Content</p>
</CardContent>
</Card>
// ❌ Avoid - Hardcoded values
<Card className="rounded-md border border-gray-200 bg-white text-gray-900">
<CardContent className="p-6">
<p className="text-gray-900">Content</p>
</CardContent>
</Card>
Consistent Spacing
Use consistent spacing throughout your application:
// ✅ Good - Consistent spacing
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Title</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p>Content</p>
<div className="flex space-x-2">
<Button>Action 1</Button>
<Button variant="outline">Action 2</Button>
</div>
</CardContent>
</Card>
</div>
Error Handling
Graceful Degradation
Handle errors gracefully and provide fallbacks:
function UserProfile({ user, error }) {
if (error) {
return (
<Card>
<CardContent className="p-6">
<div className="text-center">
<p className="text-destructive mb-2">Failed to load profile</p>
<Button onClick={() => window.location.reload()}>
Try Again
</Button>
</div>
</CardContent>
</Card>
)
}
if (!user) {
return (
<Card>
<CardContent className="p-6">
<div className="space-y-2">
<Skeleton className="h-4 w-[200px]" />
<Skeleton className="h-4 w-[150px]" />
</div>
</CardContent>
</Card>
)
}
return (
<Card>
<CardHeader>
<CardTitle>{user.name}</CardTitle>
</CardHeader>
<CardContent>
<p>{user.bio}</p>
</CardContent>
</Card>
)
}
Testing
Test Component Behavior
Write tests that focus on component behavior rather than implementation:
import { render, screen, fireEvent } from '@testing-library/react'
import { Button, Input, Label } from '@foundrykit/primitives'
test('form submission works correctly', () => {
const handleSubmit = jest.fn()
render(
<form onSubmit={handleSubmit}>
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" />
<Button type="submit">Submit</Button>
</form>
)
fireEvent.change(screen.getByLabelText(/email/i), {
target: { value: 'test@example.com' }
})
fireEvent.click(screen.getByRole('button', { name: /submit/i }))
expect(handleSubmit).toHaveBeenCalled()
})
Test Accessibility
Ensure components are accessible:
test('button is accessible', () => {
render(<Button>Click me</Button>)
const button = screen.getByRole('button', { name: /click me/i })
expect(button).toBeInTheDocument()
expect(button).toHaveAttribute('type', 'button')
})
test('form has proper labels', () => {
render(
<form>
<Label htmlFor="email">Email Address</Label>
<Input id="email" type="email" />
</form>
)
expect(screen.getByLabelText(/email address/i)).toBeInTheDocument()
})
Code Organization
Group Related Components
Organize related components together:
// components/forms/ContactForm.tsx
export function ContactForm() {
return (
<form className="space-y-4">
<div>
<Label htmlFor="name">Name</Label>
<Input id="name" name="name" required />
</div>
<div>
<Label htmlFor="email">Email</Label>
<Input id="email" name="email" type="email" required />
</div>
<Button type="submit">Send</Button>
</form>
)
}
// components/forms/NewsletterForm.tsx
export function NewsletterForm() {
return (
<form className="space-y-4">
<div>
<Label htmlFor="email">Email</Label>
<Input id="email" name="email" type="email" required />
</div>
<Button type="submit">Subscribe</Button>
</form>
)
}
Use Custom Hooks
Extract reusable logic into custom hooks:
// hooks/useForm.ts
export function useForm(initialValues) {
const [values, setValues] = useState(initialValues)
const [errors, setErrors] = useState({})
const [touched, setTouched] = useState({})
const handleChange = (e) => {
const { name, value } = e.target
setValues(prev => ({ ...prev, [name]: value }))
}
const handleBlur = (e) => {
const { name } = e.target
setTouched(prev => ({ ...prev, [name]: true }))
}
return {
values,
errors,
touched,
handleChange,
handleBlur,
setErrors
}
}
// components/forms/ContactForm.tsx
export function ContactForm() {
const { values, errors, touched, handleChange, handleBlur } = useForm({
name: '',
email: ''
})
return (
<form className="space-y-4">
<div>
<Label htmlFor="name">Name</Label>
<Input
id="name"
name="name"
value={values.name}
onChange={handleChange}
onBlur={handleBlur}
aria-invalid={errors.name && touched.name ? 'true' : 'false'}
/>
{errors.name && touched.name && (
<p className="text-sm text-destructive">{errors.name}</p>
)}
</div>
{/* ... more fields */}
</form>
)
}
Common Patterns
Modal Dialogs
Use dialogs for important actions that require user confirmation:
function DeleteConfirmation({ isOpen, onClose, onConfirm, itemName }) {
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>Confirm Deletion</DialogTitle>
<DialogDescription>
Are you sure you want to delete "{itemName}"? This action cannot be undone.
</DialogDescription>
</DialogHeader>
<div className="flex justify-end space-x-2">
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
<Button variant="destructive" onClick={onConfirm}>
Delete
</Button>
</div>
</DialogContent>
</Dialog>
)
}
Loading States
Provide clear loading feedback:
function DataTable({ data, isLoading, error }) {
if (isLoading) {
return (
<div className="space-y-2">
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-4 w-[200px]" />
<Skeleton className="h-4 w-[300px]" />
</div>
)
}
if (error) {
return (
<Card>
<CardContent className="p-6">
<p className="text-destructive">Failed to load data</p>
</CardContent>
</Card>
)
}
return (
<div>
{data.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
)
}
Next Steps
- Explore component documentation for detailed usage examples
- Learn about styling and theming to customize appearance
- Review accessibility guidelines to ensure inclusive design