Best Practices
Follow these best practices to build accessible, performant, and maintainable applications with Nim UI.
Component Usage
Import Only What You Need
Tree-shakeable imports reduce bundle size:
// ✅ Good - imports only Buttonimport { Button } from '@nim-ui/components';
// ❌ Bad - imports entire libraryimport * as NimUI from '@nim-ui/components';Use Semantic HTML
Leverage semantic HTML elements for better accessibility:
// ✅ Good - semantic structure<nav> <Button variant="ghost">Home</Button> <Button variant="ghost">About</Button></nav>
// ❌ Bad - divs for everything<div> <div onClick={goHome}>Home</div> <div onClick={goAbout}>About</div></div>Prefer Composition
Build complex UIs by composing simple components:
// ✅ Good - composable<Card> <Card.Header> <h2>Title</h2> </Card.Header> <Card.Body> <p>Content</p> </Card.Body> <Card.Footer> <Button>Action</Button> </Card.Footer></Card>
// ❌ Bad - monolithic props<Card title="Title" content="Content" actionLabel="Action" onAction={handleAction}/>Accessibility
Always Provide Labels
Every interactive element needs a label:
// ✅ Good - has accessible label<Button aria-label="Close dialog"> <XIcon /></Button>
// ❌ Bad - no label for icon-only button<Button> <XIcon /></Button>Maintain Focus Management
Handle focus properly in modals and dynamic content:
import { useEffect, useRef } from 'react';
function Modal({ isOpen, onClose }) { const closeButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => { if (isOpen) { // Focus the close button when modal opens closeButtonRef.current?.focus(); } }, [isOpen]);
return ( <div role="dialog" aria-modal="true"> <Button ref={closeButtonRef} onClick={onClose}> Close </Button> </div> );}Keyboard Navigation
Ensure all interactions work via keyboard:
function SearchBox() { const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { handleSearch(); } if (e.key === 'Escape') { clearSearch(); } };
return ( <Input onKeyDown={handleKeyDown} placeholder="Search..." /> );}Color Contrast
Ensure sufficient color contrast (WCAG AA: 4.5:1):
// ✅ Good - high contrast<div className="bg-neutral-900 text-white"> High contrast text</div>
// ❌ Bad - low contrast<div className="bg-neutral-300 text-neutral-400"> Hard to read</div>Performance
Lazy Load Heavy Components
Use React.lazy for code splitting:
import { lazy, Suspense } from 'react';import { Spinner } from '@nim-ui/components';
const HeavyChart = lazy(() => import('./HeavyChart'));
function Dashboard() { return ( <Suspense fallback={<Spinner />}> <HeavyChart /> </Suspense> );}Memoize Expensive Computations
Use useMemo for expensive calculations:
import { useMemo } from 'react';
function DataTable({ data }) { const sortedData = useMemo(() => { return data.sort((a, b) => a.name.localeCompare(b.name)); }, [data]);
return <Table data={sortedData} />;}Debounce User Input
Debounce search and autocomplete inputs:
import { useState, useEffect } from 'react';
function SearchInput() { const [query, setQuery] = useState(''); const [debouncedQuery, setDebouncedQuery] = useState('');
useEffect(() => { const timer = setTimeout(() => { setDebouncedQuery(query); }, 300);
return () => clearTimeout(timer); }, [query]);
useEffect(() => { if (debouncedQuery) { performSearch(debouncedQuery); } }, [debouncedQuery]);
return ( <Input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search..." /> );}Virtualize Long Lists
Use virtualization for long lists:
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }) { const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 50, });
return ( <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}> {virtualizer.getVirtualItems().map((virtualItem) => ( <div key={virtualItem.key}> {items[virtualItem.index]} </div> ))} </div> );}Form Handling
Validate on Submit
Validate forms on submit rather than on every keystroke:
function ContactForm() { const [errors, setErrors] = useState({});
const handleSubmit = (e: React.FormEvent) => { e.preventDefault();
const formData = new FormData(e.currentTarget); const validationErrors = validate(formData);
if (Object.keys(validationErrors).length > 0) { setErrors(validationErrors); return; }
submitForm(formData); };
return ( <form onSubmit={handleSubmit}> <Input name="email" /> {errors.email && <span>{errors.email}</span>} <Button type="submit">Submit</Button> </form> );}Use Controlled Components Wisely
Only use controlled components when necessary:
// ✅ Good - uncontrolled for simple forms<form onSubmit={handleSubmit}> <Input name="email" defaultValue="" /></form>
// ✅ Good - controlled when you need the valuefunction SearchForm() { const [query, setQuery] = useState('');
return ( <Input value={query} onChange={(e) => setQuery(e.target.value)} /> );}Show Clear Error Messages
Provide specific, actionable error messages:
// ✅ Good - specific and helpful{error && ( <p className="text-red-600"> Email must be in format: [email protected] </p>)}
// ❌ Bad - vague{error && <p>Invalid input</p>}State Management
Keep State Close to Where It’s Used
Don’t lift state unnecessarily:
// ✅ Good - state in component that uses itfunction ToggleButton() { const [isOn, setIsOn] = useState(false);
return ( <Button onClick={() => setIsOn(!isOn)}> {isOn ? 'On' : 'Off'} </Button> );}
// ❌ Bad - unnecessarily lifted statefunction Parent() { const [isOn, setIsOn] = useState(false);
return <Child isOn={isOn} setIsOn={setIsOn} />;}Use Context for Global State
Use Context API for truly global state:
import { createContext, useContext } from 'react';
const ThemeContext = createContext();
export function ThemeProvider({ children }) { const [theme, setTheme] = useState('light');
return ( <ThemeContext.Provider value={{ theme, setTheme }}> {children} </ThemeContext.Provider> );}
export function useTheme() { return useContext(ThemeContext);}Styling
Extend, Don’t Override
Extend component styles with className:
// ✅ Good - extends base styles<Button className="rounded-full shadow-lg"> Custom</Button>
// ❌ Bad - inline styles that override<Button style={{ borderRadius: '9999px' }}> Custom</Button>Use Tailwind Utilities
Prefer Tailwind utilities over custom CSS:
// ✅ Good - Tailwind utilities<div className="flex items-center justify-between gap-4"> Content</div>
// ❌ Bad - custom CSS<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '1rem'}}> Content</div>Follow the Design System
Use design tokens consistently:
// ✅ Good - uses design tokens<div className="text-neutral-900 dark:text-neutral-100"> Text</div>
// ❌ Bad - arbitrary colors<div className="text-[#1a1a1a]"> Text</div>TypeScript
Define Proper Types
Use TypeScript for type safety:
// ✅ Good - proper typesinterface User { id: string; name: string; email: string;}
function UserCard({ user }: { user: User }) { return <Card>{user.name}</Card>;}
// ❌ Bad - any typesfunction UserCard({ user }: { user: any }) { return <Card>{user.name}</Card>;}Use Type Inference
Let TypeScript infer types when possible:
// ✅ Good - inferred typesconst [count, setCount] = useState(0);const users = data.map(user => user.name);
// ❌ Bad - unnecessary explicit typesconst [count, setCount] = useState<number>(0);const users: string[] = data.map((user: User) => user.name);Testing
Test User Interactions
Focus on testing user behavior:
import { render, screen, fireEvent } from '@testing-library/react';
test('button calls onClick when clicked', () => { const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledOnce();});Use Semantic Queries
Query by accessible roles and labels:
// ✅ Good - semantic queryconst button = screen.getByRole('button', { name: 'Submit' });
// ❌ Bad - implementation detailconst button = screen.getByClassName('btn-primary');Security
Sanitize User Input
Never render unsanitized user content:
import DOMPurify from 'dompurify';
function UserContent({ content }) { const sanitized = DOMPurify.sanitize(content);
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;}Validate on Server
Always validate on the server, not just client:
// Client-side validation for UXfunction SignupForm() { const handleSubmit = async (data) => { // Client validation if (!data.email) { setError('Email required'); return; }
// Server validates again const response = await fetch('/api/signup', { method: 'POST', body: JSON.stringify(data), }); };}Documentation
Comment Complex Logic
Add comments for non-obvious code:
function calculateDiscount(price: number, tier: string) { // Corporate tier gets 30% bulk discount // Enterprise tier gets 50% volume discount const multiplier = tier === 'enterprise' ? 0.5 : 0.7;
return price * multiplier;}Use JSDoc for Public APIs
Document public functions and components:
/** * Formats a price with currency symbol * * @param amount - The numeric amount * @param currency - Currency code (USD, EUR, etc.) * @returns Formatted price string * * @example * formatPrice(1999, 'USD') // "$19.99" */function formatPrice(amount: number, currency: string): string { return new Intl.NumberFormat('en-US', { style: 'currency', currency, }).format(amount / 100);}Checklist
Before deploying to production:
- All components are accessible (WCAG AA)
- Loading states for async operations
- Error handling and error boundaries
- Responsive design tested on mobile
- Dark mode support verified
- Bundle size optimized
- TypeScript with no errors
- Tests passing
- SEO meta tags added
- Performance budgets met