NimUI
Guides

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

Resources

On this page