--- name: react-component description: Create React components with TypeScript, following best practices for hooks, state management, accessibility, and testing. Use when building new UI components or refactoring existing ones. --- # React Component Development Modern React patterns with TypeScript, hooks, and best practices. ## Component Structure ### Basic Component Template ```tsx 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 = 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.displayName = 'Button'; ``` ## Hooks Best Practices ### useState ```tsx // 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({ firstName: '', lastName: '', email: '', }); // Update single field const updateField = (field: keyof FormData, value: string) => { setFormData(prev => ({ ...prev, [field]: value })); }; ``` ### useEffect ```tsx // 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 ```tsx // 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 ```tsx // DOM references const inputRef = useRef(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(value: T): T | undefined { const ref = useRef(); useEffect(() => { ref.current = value; }); return ref.current; } ``` ## Custom Hooks ### Data Fetching Hook ```tsx interface UseFetchResult { data: T | null; loading: boolean; error: Error | null; refetch: () => void; } function useFetch(url: string): UseFetchResult { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(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 ```tsx interface UseFormOptions { initialValues: T; validate?: (values: T) => Partial>; onSubmit: (values: T) => void | Promise; } function useForm>({ initialValues, validate, onSubmit, }: UseFormOptions) { const [values, setValues] = useState(initialValues); const [errors, setErrors] = useState>>({}); 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 ```tsx 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 ```tsx interface TabsContextValue { activeTab: string; setActiveTab: (id: string) => void; } const TabsContext = createContext(null); function Tabs({ children, defaultTab }: { children: ReactNode; defaultTab: string }) { const [activeTab, setActiveTab] = useState(defaultTab); return (
{children}
); } function TabList({ children }: { children: ReactNode }) { return
{children}
; } function Tab({ id, children }: { id: string; children: ReactNode }) { const context = useContext(TabsContext); if (!context) throw new Error('Tab must be used within Tabs'); return ( ); } 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
{children}
; } // Usage Tab 1 Tab 2 Content 1 Content 2 ``` ### Render Props ```tsx 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 {({ x, y }) =>
Mouse: {x}, {y}
}
``` ### Higher-Order Components ```tsx function withLoading

(Component: ComponentType

) { return function WithLoadingComponent({ loading, ...props }: P & { loading: boolean }) { if (loading) return ; return ; }; } // Usage const UserListWithLoading = withLoading(UserList); ``` ## Accessibility (a11y) ### ARIA Attributes ```tsx // Button with loading state // Modal dialog

// Live region for dynamic content
{message}
``` ### Keyboard Navigation ```tsx 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 (
    {items.map((item, index) => (
  • {item.label}
  • ))}
); } ``` ### Focus Management ```tsx function Modal({ isOpen, onClose, children }: ModalProps) { const modalRef = useRef(null); const previousFocus = useRef(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 (
{children}
); } ``` ## Testing ### Component Testing ```tsx 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(); expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument(); }); it('calls onClick when clicked', async () => { const handleClick = jest.fn(); render(); await userEvent.click(screen.getByRole('button')); expect(handleClick).toHaveBeenCalledTimes(1); }); it('does not call onClick when disabled', async () => { const handleClick = jest.fn(); render(); await userEvent.click(screen.getByRole('button')); expect(handleClick).not.toHaveBeenCalled(); }); it('shows loading spinner when loading', () => { render(); expect(screen.getByRole('button')).toHaveAttribute('aria-busy', 'true'); }); }); ``` ### Hook Testing ```tsx 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 ```tsx // Memoize component to prevent unnecessary re-renders const ExpensiveList = memo(({ items }: { items: Item[] }) => { return (
    {items.map(item => (
  • {item.name}
  • ))}
); }); // Custom comparison function const UserCard = memo( ({ user }: { user: User }) =>
{user.name}
, (prevProps, nextProps) => prevProps.user.id === nextProps.user.id ); ``` ### Code Splitting ```tsx import { lazy, Suspense } from 'react'; // Lazy load components const Dashboard = lazy(() => import('./Dashboard')); const Settings = lazy(() => import('./Settings')); function App() { return ( }> } /> } /> ); } ``` ### Virtualization ```tsx import { useVirtualizer } from '@tanstack/react-virtual'; function VirtualList({ items }: { items: Item[] }) { const parentRef = useRef(null); const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 50, }); return (
{virtualizer.getVirtualItems().map(virtualItem => (
{items[virtualItem.index].name}
))}
); } ``` ## 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 |