Best practices
Patterns and guidelines for building scalable, maintainable interfaces with UI Lab components.
Component composition
Use compound components effectively
Components like Card, Dialog, and Tabs use the compound component pattern. Leverage their slots to build consistent layouts:
import { Card, Button } from '@ui-lab/core';
// ✅ Use compound components
export default function UserCard({ user }) {
return (
<Card>
<Card.Header>
<Card.Title>{user.name}</Card.Title>
<Card.Description>{user.email}</Card.Description>
</Card.Header>
<Card.Content>
{/* Content here */}
</Card.Content>
<Card.Footer>
<Button>Edit</Button>
</Card.Footer>
</Card>
);
}This approach maintains consistent spacing and semantic structure across your app.
Wrap components for project-specific needs
Create wrapper components for common patterns in your project:
// components/PrimaryButton.tsx
import { Button, type ButtonProps } from '@ui-lab/core';
export default function PrimaryButton(props: ButtonProps) {
return <Button variant="primary" size="md" {...props} />;
}
// components/FormField.tsx
import { Label, Input } from '@ui-lab/core';
interface FormFieldProps {
label: string;
error?: string;
[key: string]: any;
}
export default function FormField({
label,
error,
...inputProps
}: FormFieldProps) {
return (
<div>
<Label htmlFor={inputProps.id}>{label}</Label>
<Input
aria-invalid={!!error}
aria-describedby={error ? `${inputProps.id}-error` : undefined}
{...inputProps}
/>
{error && (
<p id={`${inputProps.id}-error`} className="text-destructive-500 text-sm mt-1">
{error}
</p>
)}
</div>
);
}Wrapper components let you enforce patterns and reduce repetition without forking component source.
State management
Keep component state simple
UI Lab components are controlled components. Manage form state with React hooks or your state management solution:
import { useState } from 'react';
import { Button, Input, Card } from '@ui-lab/core';
export default function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
try {
const response = await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify(formData),
});
if (!response.ok) {
setErrors({ submit: 'Failed to send message' });
}
} finally {
setIsSubmitting(false);
}
};
const handleChange = (field: string) => (e) => {
setFormData(prev => ({
...prev,
[field]: e.target.value
}));
// Clear error when user starts typing
if (errors[field]) {
setErrors(prev => ({
...prev,
[field]: ''
}));
}
};
return (
<Card>
<Card.Header>
<Card.Title>Contact us</Card.Title>
</Card.Header>
<form onSubmit={handleSubmit}>
<Card.Content className="space-y-4">
<Input
placeholder="Your name"
value={formData.name}
onChange={handleChange('name')}
/>
<Input
type="email"
placeholder="your@email.com"
value={formData.email}
onChange={handleChange('email')}
/>
<textarea
placeholder="Your message"
value={formData.message}
onChange={handleChange('message')}
className="w-full p-2 rounded border border-background-700"
/>
</Card.Content>
<Card.Footer>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Sending...' : 'Send'}
</Button>
</Card.Footer>
</form>
</Card>
);
}Separate concerns with custom hooks
Extract form logic into custom hooks for reuse:
// hooks/useForm.ts
import { useState } from 'react';
export function useForm(onSubmit) {
const [values, setValues] = useState({});
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setValues(prev => ({ ...prev, [name]: value }));
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
};
const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
await onSubmit(values);
setIsSubmitting(false);
};
return { values, errors, isSubmitting, handleChange, handleSubmit };
}
// Usage in component
import { useForm } from '@/hooks/useForm';
import { Card, Button, Input } from '@ui-lab/core';
export default function SignupForm() {
const { values, handleChange, handleSubmit, isSubmitting } = useForm(
async (data) => {
await fetch('/api/signup', {
method: 'POST',
body: JSON.stringify(data),
});
}
);
return (
<form onSubmit={handleSubmit}>
<Input name="email" value={values.email} onChange={handleChange} />
<Button type="submit" disabled={isSubmitting}>Sign up</Button>
</form>
);
}Performance optimization
Use React.memo for list items
When rendering long lists, memoize list items to prevent unnecessary re-renders:
import { memo } from 'react';
import { Card, Button } from '@ui-lab/core';
const ListItem = memo(function ListItem({ item, onDelete }) {
return (
<Card className="flex items-center justify-between p-4">
<div>
<p className="font-semibold">{item.name}</p>
<p className="text-foreground-400 text-sm">{item.description}</p>
</div>
<Button variant="destructive" onClick={() => onDelete(item.id)}>
Delete
</Button>
</Card>
);
});
export default function ItemList({ items, onDelete }) {
return (
<div className="space-y-2">
{items.map(item => (
<ListItem key={item.id} item={item} onDelete={onDelete} />
))}
</div>
);
}Lazy load dialogs and modals
Use dynamic imports for heavy components shown conditionally:
import { lazy, Suspense, useState } from 'react';
import { Button } from '@ui-lab/core';
const SettingsDialog = lazy(() => import('./SettingsDialog'));
export default function App() {
const [showSettings, setShowSettings] = useState(false);
return (
<>
<Button onClick={() => setShowSettings(true)}>
Settings
</Button>
{showSettings && (
<Suspense fallback={<div>Loading...</div>}>
<SettingsDialog onClose={() => setShowSettings(false)} />
</Suspense>
)}
</>
);
}Minimize className computations
Avoid computing className inline. Move it outside render:
// ❌ Don't do this - className computed every render
export default function Item({ isActive }) {
return (
<div className={isActive ? 'bg-accent-500' : 'bg-background-800'}>
Item
</div>
);
}
// ✅ Do this - className computed once
const itemClassName = (isActive: boolean) =>
isActive ? 'bg-accent-500' : 'bg-background-800';
export default function Item({ isActive }) {
return <div className={itemClassName(isActive)}>Item</div>;
}TypeScript best practices
Type component props properly
Always define props interfaces for better type safety and documentation:
import { Button, Card } from '@ui-lab/core';
interface UserCardProps {
userId: string;
onEdit?: (id: string) => void;
onDelete?: (id: string) => void;
}
export default function UserCard({
userId,
onEdit,
onDelete,
}: UserCardProps) {
return (
<Card>
<Card.Header>
<Card.Title>User #{userId}</Card.Title>
</Card.Header>
<Card.Footer className="gap-2">
{onEdit && <Button onClick={() => onEdit(userId)}>Edit</Button>}
{onDelete && (
<Button variant="destructive" onClick={() => onDelete(userId)}>
Delete
</Button>
)}
</Card.Footer>
</Card>
);
}Extend component prop types
Reuse UI Lab component types when creating wrappers:
import { Button, type ButtonProps } from '@ui-lab/core';
interface PrimaryButtonProps extends ButtonProps {
icon?: React.ReactNode;
}
export default function PrimaryButton({
icon,
...props
}: PrimaryButtonProps) {
return (
<Button variant="primary" {...props}>
{icon && <span className="mr-2">{icon}</span>}
{props.children}
</Button>
);
}Responsive design
Use Tailwind responsive prefixes
Build responsive layouts with Tailwind's responsive utilities:
import { Card, Button } from '@ui-lab/core';
export default function Dashboard() {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<Card.Header>
<Card.Title className="text-lg md:text-xl">
Revenue
</Card.Title>
</Card.Header>
<Card.Content>
<p className="text-2xl md:text-3xl font-bold">$4,231</p>
</Card.Content>
</Card>
{/* More cards */}
</div>
);
}Test on real devices
Browser DevTools responsive mode is helpful, but test on real devices. Common breakpoints:
- •
sm: 640px— Small phones - •
md: 768px— Tablets - •
lg: 1024px— Laptops - •
xl: 1280px— Desktops
Common patterns
Conditional rendering
Use concise conditional rendering:
// ✅ Good
{isLoading && <div>Loading...</div>}
{error && <p className="text-destructive-500">{error}</p>}
{data && <Card>{data.name}</Card>}
// ❌ Avoid
{isLoading ? <div>Loading...</div> : null}
{error ? <p>{error}</p> : null}
{data ? <Card>{data.name}</Card> : null}Event handler naming
Name handlers consistently with handle prefix:
export default function Form() {
const handleSubmit = (e) => { /* ... */ };
const handleReset = () => { /* ... */ };
const handleInputChange = (e) => { /* ... */ };
const handleDelete = (id) => { /* ... */ };
return (
<form onSubmit={handleSubmit}>
<Input onChange={handleInputChange} />
<Button type="submit">Submit</Button>
<Button onClick={handleReset}>Reset</Button>
<Button onClick={() => handleDelete(id)}>Delete</Button>
</form>
);
}