Skip to content

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 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: [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 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

Resources