Best Practices
Tips and recommendations for building with Nim UI
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 Button
import { Button } from '@nim-ui/components';
// ❌ Bad - imports entire library
import * 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 value
function 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: user@example.com</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 it
function ToggleButton() {
const [isOn, setIsOn] = useState(false);
return (
<Button onClick={() => setIsOn(!isOn)}>
{isOn ? 'On' : 'Off'}
</Button>
);
}
// ❌ Bad - unnecessarily lifted state
function 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 types
interface User {
id: string;
name: string;
email: string;
}
function UserCard({ user }: { user: User }) {
return <Card>{user.name}</Card>;
}
// ❌ Bad - any types
function UserCard({ user }: { user: any }) {
return <Card>{user.name}</Card>;
}Use Type Inference
Let TypeScript infer types when possible:
// ✅ Good - inferred types
const [count, setCount] = useState(0);
const users = data.map(user => user.name);
// ❌ Bad - unnecessary explicit types
const [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 query
const button = screen.getByRole('button', { name: 'Submit' });
// ❌ Bad - implementation detail
const 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 UX
function 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