react-component

React Component Development

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "react-component" with this command: npx skills add vapvarun/claude-backup/vapvarun-claude-backup-react-component

React Component Development

Modern React patterns with TypeScript, hooks, and best practices.

Component Structure

Basic Component Template

import { useState, useCallback, memo } from 'react'; import type { FC, ReactNode } from 'react'; import styles from './Button.module.css';

interface ButtonProps { /** Button content / children: ReactNode; /* Button variant style / variant?: 'primary' | 'secondary' | 'danger'; /* Size of the button / size?: 'sm' | 'md' | 'lg'; /* Whether the button is disabled / disabled?: boolean; /* Loading state / loading?: boolean; /* Click handler / onClick?: () => void; /* Additional CSS classes */ className?: string; }

export const Button: FC<ButtonProps> = memo(({ children, variant = 'primary', size = 'md', disabled = false, loading = false, onClick, className, }) => { const handleClick = useCallback(() => { if (!disabled && !loading && onClick) { onClick(); } }, [disabled, loading, onClick]);

return ( <button type="button" className={${styles.button} ${styles[variant]} ${styles[size]} ${className ?? ''}} disabled={disabled || loading} onClick={handleClick} aria-busy={loading} > {loading ? <Spinner size="sm" /> : children} </button> ); });

Button.displayName = 'Button';

Hooks Best Practices

useState

// BAD: Multiple related states const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const [email, setEmail] = useState('');

// GOOD: Group related state interface FormData { firstName: string; lastName: string; email: string; }

const [formData, setFormData] = useState<FormData>({ firstName: '', lastName: '', email: '', });

// Update single field const updateField = (field: keyof FormData, value: string) => { setFormData(prev => ({ ...prev, [field]: value })); };

useEffect

// BAD: Missing cleanup useEffect(() => { const subscription = api.subscribe(handler); // Memory leak - no cleanup! }, []);

// GOOD: Proper cleanup useEffect(() => { const subscription = api.subscribe(handler); return () => subscription.unsubscribe(); }, [handler]);

// BAD: Stale closure useEffect(() => { const interval = setInterval(() => { setCount(count + 1); // count is stale! }, 1000); return () => clearInterval(interval); }, []); // Missing count dependency

// GOOD: Functional update useEffect(() => { const interval = setInterval(() => { setCount(prev => prev + 1); // Always uses latest }, 1000); return () => clearInterval(interval); }, []);

useCallback & useMemo

// useCallback - Memoize functions const handleSubmit = useCallback(async (data: FormData) => { await api.submit(data); onSuccess(); }, [onSuccess]);

// useMemo - Memoize expensive computations const sortedItems = useMemo(() => { return [...items].sort((a, b) => a.name.localeCompare(b.name)); }, [items]);

// useMemo - Memoize objects/arrays passed as props const config = useMemo(() => ({ theme: 'dark', locale: 'en', }), []); // Stable reference

// DON'T over-memoize simple values // BAD const doubled = useMemo(() => count * 2, [count]);

// GOOD - Simple math doesn't need memoization const doubled = count * 2;

useRef

// DOM references const inputRef = useRef<HTMLInputElement>(null);

const focusInput = () => { inputRef.current?.focus(); };

// Mutable values that don't trigger re-renders const renderCount = useRef(0); renderCount.current += 1;

// Previous value pattern function usePrevious<T>(value: T): T | undefined { const ref = useRef<T>(); useEffect(() => { ref.current = value; }); return ref.current; }

Custom Hooks

Data Fetching Hook

interface UseFetchResult<T> { data: T | null; loading: boolean; error: Error | null; refetch: () => void; }

function useFetch<T>(url: string): UseFetchResult<T> { const [data, setData] = useState<T | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<Error | null>(null);

const fetchData = useCallback(async () => { try { setLoading(true); setError(null); const response = await fetch(url); if (!response.ok) throw new Error('Fetch failed'); const json = await response.json(); setData(json); } catch (err) { setError(err instanceof Error ? err : new Error('Unknown error')); } finally { setLoading(false); } }, [url]);

useEffect(() => { fetchData(); }, [fetchData]);

return { data, loading, error, refetch: fetchData }; }

Form Hook

interface UseFormOptions<T> { initialValues: T; validate?: (values: T) => Partial<Record<keyof T, string>>; onSubmit: (values: T) => void | Promise<void>; }

function useForm<T extends Record<string, any>>({ initialValues, validate, onSubmit, }: UseFormOptions<T>) { const [values, setValues] = useState<T>(initialValues); const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({}); const [submitting, setSubmitting] = useState(false);

const handleChange = useCallback((field: keyof T, value: any) => { setValues(prev => ({ ...prev, [field]: value })); // Clear error on change setErrors(prev => ({ ...prev, [field]: undefined })); }, []);

const handleSubmit = useCallback(async (e: React.FormEvent) => { e.preventDefault();

if (validate) {
  const validationErrors = validate(values);
  if (Object.keys(validationErrors).length > 0) {
    setErrors(validationErrors);
    return;
  }
}

setSubmitting(true);
try {
  await onSubmit(values);
} finally {
  setSubmitting(false);
}

}, [values, validate, onSubmit]);

const reset = useCallback(() => { setValues(initialValues); setErrors({}); }, [initialValues]);

return { values, errors, submitting, handleChange, handleSubmit, reset }; }

Toggle Hook

function useToggle(initial = false): [boolean, () => void, () => void, () => void] { const [value, setValue] = useState(initial);

const toggle = useCallback(() => setValue(v => !v), []); const setTrue = useCallback(() => setValue(true), []); const setFalse = useCallback(() => setValue(false), []);

return [value, toggle, setTrue, setFalse]; }

// Usage const [isOpen, toggleOpen, open, close] = useToggle(false);

Component Patterns

Compound Components

interface TabsContextValue { activeTab: string; setActiveTab: (id: string) => void; }

const TabsContext = createContext<TabsContextValue | null>(null);

function Tabs({ children, defaultTab }: { children: ReactNode; defaultTab: string }) { const [activeTab, setActiveTab] = useState(defaultTab);

return ( <TabsContext.Provider value={{ activeTab, setActiveTab }}> <div className="tabs">{children}</div> </TabsContext.Provider> ); }

function TabList({ children }: { children: ReactNode }) { return <div role="tablist">{children}</div>; }

function Tab({ id, children }: { id: string; children: ReactNode }) { const context = useContext(TabsContext); if (!context) throw new Error('Tab must be used within Tabs');

return ( <button role="tab" aria-selected={context.activeTab === id} onClick={() => context.setActiveTab(id)} > {children} </button> ); }

function TabPanel({ id, children }: { id: string; children: ReactNode }) { const context = useContext(TabsContext); if (!context) throw new Error('TabPanel must be used within Tabs'); if (context.activeTab !== id) return null;

return <div role="tabpanel">{children}</div>; }

// Usage <Tabs defaultTab="tab1"> <TabList> <Tab id="tab1">Tab 1</Tab> <Tab id="tab2">Tab 2</Tab> </TabList> <TabPanel id="tab1">Content 1</TabPanel> <TabPanel id="tab2">Content 2</TabPanel> </Tabs>

Render Props

interface MousePosition { x: number; y: number; }

interface MouseTrackerProps { children: (position: MousePosition) => ReactNode; }

function MouseTracker({ children }: MouseTrackerProps) { const [position, setPosition] = useState({ x: 0, y: 0 });

useEffect(() => { const handleMove = (e: MouseEvent) => { setPosition({ x: e.clientX, y: e.clientY }); };

window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);

}, []);

return <>{children(position)}</>; }

// Usage <MouseTracker> {({ x, y }) => <div>Mouse: {x}, {y}</div>} </MouseTracker>

Higher-Order Components

function withLoading<P extends object>(Component: ComponentType<P>) { return function WithLoadingComponent({ loading, ...props }: P & { loading: boolean }) { if (loading) return <Spinner />; return <Component {...(props as P)} />; }; }

// Usage const UserListWithLoading = withLoading(UserList); <UserListWithLoading loading={isLoading} users={users} />

Accessibility (a11y)

ARIA Attributes

// Button with loading state <button aria-busy={loading} aria-disabled={disabled} aria-describedby={hasError ? 'error-message' : undefined}

{loading ? 'Loading...' : 'Submit'} </button>

// Modal dialog <div role="dialog" aria-modal="true" aria-labelledby="modal-title" aria-describedby="modal-description"

<h2 id="modal-title">Confirm Action</h2> <p id="modal-description">Are you sure?</p> </div>

// Live region for dynamic content <div aria-live="polite" aria-atomic="true"> {message} </div>

Keyboard Navigation

function Menu({ items }: { items: MenuItem[] }) { const [focusIndex, setFocusIndex] = useState(0);

const handleKeyDown = (e: React.KeyboardEvent) => { switch (e.key) { case 'ArrowDown': e.preventDefault(); setFocusIndex(i => Math.min(i + 1, items.length - 1)); break; case 'ArrowUp': e.preventDefault(); setFocusIndex(i => Math.max(i - 1, 0)); break; case 'Home': e.preventDefault(); setFocusIndex(0); break; case 'End': e.preventDefault(); setFocusIndex(items.length - 1); break; } };

return ( <ul role="menu" onKeyDown={handleKeyDown}> {items.map((item, index) => ( <li key={item.id} role="menuitem" tabIndex={index === focusIndex ? 0 : -1} > {item.label} </li> ))} </ul> ); }

Focus Management

function Modal({ isOpen, onClose, children }: ModalProps) { const modalRef = useRef<HTMLDivElement>(null); const previousFocus = useRef<HTMLElement | null>(null);

useEffect(() => { if (isOpen) { // Save current focus previousFocus.current = document.activeElement as HTMLElement; // Focus modal modalRef.current?.focus(); } else { // Restore focus previousFocus.current?.focus(); } }, [isOpen]);

// Trap focus inside modal const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Tab') { const focusableElements = modalRef.current?.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); // ... trap focus logic } if (e.key === 'Escape') { onClose(); } };

if (!isOpen) return null;

return ( <div ref={modalRef} role="dialog" aria-modal="true" tabIndex={-1} onKeyDown={handleKeyDown} > {children} </div> ); }

Testing

Component Testing

import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Button } from './Button';

describe('Button', () => { it('renders children', () => { render(<Button>Click me</Button>); expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument(); });

it('calls onClick when clicked', async () => { const handleClick = jest.fn(); render(<Button onClick={handleClick}>Click</Button>);

await userEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);

});

it('does not call onClick when disabled', async () => { const handleClick = jest.fn(); render(<Button onClick={handleClick} disabled>Click</Button>);

await userEvent.click(screen.getByRole('button'));
expect(handleClick).not.toHaveBeenCalled();

});

it('shows loading spinner when loading', () => { render(<Button loading>Submit</Button>); expect(screen.getByRole('button')).toHaveAttribute('aria-busy', 'true'); }); });

Hook Testing

import { renderHook, act } from '@testing-library/react'; import { useToggle } from './useToggle';

describe('useToggle', () => { it('initializes with default value', () => { const { result } = renderHook(() => useToggle(false)); expect(result.current[0]).toBe(false); });

it('toggles value', () => { const { result } = renderHook(() => useToggle(false));

act(() => {
  result.current[1](); // toggle
});

expect(result.current[0]).toBe(true);

}); });

Performance Optimization

Memoization

// Memoize component to prevent unnecessary re-renders const ExpensiveList = memo(({ items }: { items: Item[] }) => { return ( <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> ); });

// Custom comparison function const UserCard = memo( ({ user }: { user: User }) => <div>{user.name}</div>, (prevProps, nextProps) => prevProps.user.id === nextProps.user.id );

Code Splitting

import { lazy, Suspense } from 'react';

// Lazy load components const Dashboard = lazy(() => import('./Dashboard')); const Settings = lazy(() => import('./Settings'));

function App() { return ( <Suspense fallback={<LoadingSpinner />}> <Routes> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/settings" element={<Settings />} /> </Routes> </Suspense> ); }

Virtualization

import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualList({ items }: { items: Item[] }) { 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' }}> <div style={{ height: ${virtualizer.getTotalSize()}px, position: 'relative' }}> {virtualizer.getVirtualItems().map(virtualItem => ( <div key={virtualItem.key} style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: ${virtualItem.size}px, transform: translateY(${virtualItem.start}px), }} > {items[virtualItem.index].name} </div> ))} </div> </div> ); }

Common Mistakes

Mistake Problem Solution

State mutation state.push(item)

[...state, item]

Missing key List renders slow Use unique key prop

Object in deps Infinite loop useMemo for objects

Missing cleanup Memory leak Return cleanup function

Stale closure Wrong values Add to dependency array

Over-rendering Slow UI React.memo, useMemo

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

wp-theme-development

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

typescript

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

code-review

No summary provided by upstream source.

Repository SourceNeeds Review