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 |
More from vapvarun/claude-backup
php
Modern PHP development best practices including PHP 8.x features, OOP patterns, error handling, security, testing, and performance optimization. Use when writing PHP code, reviewing PHP projects, debugging PHP issues, or implementing PHP features outside of WordPress/Laravel specific contexts.
45laravel
Complete Laravel development guide covering Eloquent, Blade, testing with Pest/PHPUnit, queues, caching, API resources, migrations, and Laravel best practices. Use when building Laravel applications, writing Laravel code, implementing features in Laravel, debugging Laravel issues, or when user mentions Laravel, Eloquent, Blade, Artisan, or PHP frameworks.
23email-marketing
Create email marketing campaigns including newsletters, drip sequences, promotional emails, and transactional emails. Use when writing email copy, designing email templates, or planning email automation.
14javascript
Write modern JavaScript/ES6+ code following best practices for performance, security, and maintainability. Use when writing JS code, fixing bugs, or implementing frontend functionality.
14html-markup
Write semantic, accessible HTML5 markup following best practices for structure, SEO, and accessibility. Use when creating HTML templates, fixing markup issues, or building web page structures.
12landing-page
Create high-converting landing pages with persuasive copy, clear CTAs, social proof, and optimized structure. Use when building sales pages, product pages, lead capture pages, or conversion-focused pages.
12