FoundryKit

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

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

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