# React Patterns Production-ready patterns for building scalable React applications with TypeScript. --- ## Table of Contents - [Component Composition](#component-composition) - [Custom Hooks](#custom-hooks) - [State Management](#state-management) - [Performance Patterns](#performance-patterns) - [Error Boundaries](#error-boundaries) - [Anti-Patterns](#anti-patterns) --- ## Component Composition ### Compound Components Use compound components when building reusable UI components with multiple related parts. ```tsx // Compound component pattern for a Select interface SelectContextType { value: string; onChange: (value: string) => void; } const SelectContext = createContext(null); function Select({ children, value, onChange }: { children: React.ReactNode; value: string; onChange: (value: string) => void; }) { return (
{children}
); } function SelectTrigger({ children }: { children: React.ReactNode }) { const context = useContext(SelectContext); if (!context) throw new Error('SelectTrigger must be used within Select'); return ( ); } function SelectOption({ value, children }: { value: string; children: React.ReactNode }) { const context = useContext(SelectContext); if (!context) throw new Error('SelectOption must be used within Select'); return (
context.onChange(value)} className={`px-4 py-2 cursor-pointer hover:bg-gray-100 ${ context.value === value ? 'bg-blue-50' : '' }`} > {children}
); } // Attach sub-components Select.Trigger = SelectTrigger; Select.Option = SelectOption; // Usage ``` ### Render Props Use render props when you need to share behavior with flexible rendering. ```tsx interface MousePosition { x: number; y: number; } function MouseTracker({ render }: { render: (pos: MousePosition) => React.ReactNode }) { const [position, setPosition] = useState({ x: 0, y: 0 }); useEffect(() => { const handleMouseMove = (e: MouseEvent) => { setPosition({ x: e.clientX, y: e.clientY }); }; window.addEventListener('mousemove', handleMouseMove); return () => window.removeEventListener('mousemove', handleMouseMove); }, []); return <>{render(position)}; } // Usage (
Mouse position: {x}, {y}
)} /> ``` ### Higher-Order Components (HOC) Use HOCs for cross-cutting concerns like authentication or logging. ```tsx function withAuth

(WrappedComponent: React.ComponentType

) { return function AuthenticatedComponent(props: P) { const { user, isLoading } = useAuth(); if (isLoading) return ; if (!user) return ; return ; }; } // Usage const ProtectedDashboard = withAuth(Dashboard); ``` --- ## Custom Hooks ### useAsync - Handle async operations ```tsx interface AsyncState { data: T | null; error: Error | null; status: 'idle' | 'loading' | 'success' | 'error'; } function useAsync(asyncFn: () => Promise, deps: any[] = []) { const [state, setState] = useState>({ data: null, error: null, status: 'idle', }); const execute = useCallback(async () => { setState({ data: null, error: null, status: 'loading' }); try { const data = await asyncFn(); setState({ data, error: null, status: 'success' }); } catch (error) { setState({ data: null, error: error as Error, status: 'error' }); } }, deps); useEffect(() => { execute(); }, [execute]); return { ...state, refetch: execute }; } // Usage function UserProfile({ userId }: { userId: string }) { const { data: user, status, error, refetch } = useAsync( () => fetchUser(userId), [userId] ); if (status === 'loading') return ; if (status === 'error') return ; if (!user) return null; return ; } ``` ### useDebounce - Debounce values ```tsx function useDebounce(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const timer = setTimeout(() => setDebouncedValue(value), delay); return () => clearTimeout(timer); }, [value, delay]); return debouncedValue; } // Usage function SearchInput() { const [query, setQuery] = useState(''); const debouncedQuery = useDebounce(query, 300); useEffect(() => { if (debouncedQuery) { searchAPI(debouncedQuery); } }, [debouncedQuery]); return setQuery(e.target.value)} />; } ``` ### useLocalStorage - Persist state ```tsx function useLocalStorage(key: string, initialValue: T) { const [storedValue, setStoredValue] = useState(() => { if (typeof window === 'undefined') return initialValue; try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch { return initialValue; } }); const setValue = useCallback((value: T | ((val: T) => T)) => { try { const valueToStore = value instanceof Function ? value(storedValue) : value; setStoredValue(valueToStore); if (typeof window !== 'undefined') { window.localStorage.setItem(key, JSON.stringify(valueToStore)); } } catch (error) { console.error('Error saving to localStorage:', error); } }, [key, storedValue]); return [storedValue, setValue] as const; } // Usage const [theme, setTheme] = useLocalStorage('theme', 'light'); ``` ### useMediaQuery - Responsive design ```tsx function useMediaQuery(query: string): boolean { const [matches, setMatches] = useState(false); useEffect(() => { const media = window.matchMedia(query); setMatches(media.matches); const listener = (e: MediaQueryListEvent) => setMatches(e.matches); media.addEventListener('change', listener); return () => media.removeEventListener('change', listener); }, [query]); return matches; } // Usage function ResponsiveNav() { const isMobile = useMediaQuery('(max-width: 768px)'); return isMobile ? : ; } ``` ### usePrevious - Track previous values ```tsx function usePrevious(value: T): T | undefined { const ref = useRef(); useEffect(() => { ref.current = value; }, [value]); return ref.current; } // Usage function Counter() { const [count, setCount] = useState(0); const prevCount = usePrevious(count); return (

Current: {count}, Previous: {prevCount}
); } ``` --- ## State Management ### Context with Reducer For complex state that multiple components need to access. ```tsx // types.ts interface CartItem { id: string; name: string; price: number; quantity: number; } interface CartState { items: CartItem[]; total: number; } type CartAction = | { type: 'ADD_ITEM'; payload: CartItem } | { type: 'REMOVE_ITEM'; payload: string } | { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } } | { type: 'CLEAR_CART' }; // reducer.ts function cartReducer(state: CartState, action: CartAction): CartState { switch (action.type) { case 'ADD_ITEM': { const existingItem = state.items.find(i => i.id === action.payload.id); if (existingItem) { return { ...state, items: state.items.map(item => item.id === action.payload.id ? { ...item, quantity: item.quantity + 1 } : item ), }; } return { ...state, items: [...state.items, { ...action.payload, quantity: 1 }], }; } case 'REMOVE_ITEM': return { ...state, items: state.items.filter(i => i.id !== action.payload), }; case 'UPDATE_QUANTITY': return { ...state, items: state.items.map(item => item.id === action.payload.id ? { ...item, quantity: action.payload.quantity } : item ), }; case 'CLEAR_CART': return { items: [], total: 0 }; default: return state; } } // context.tsx const CartContext = createContext<{ state: CartState; dispatch: React.Dispatch; } | null>(null); function CartProvider({ children }: { children: React.ReactNode }) { const [state, dispatch] = useReducer(cartReducer, { items: [], total: 0 }); // Compute total whenever items change const stateWithTotal = useMemo(() => ({ ...state, total: state.items.reduce((sum, item) => sum + item.price * item.quantity, 0), }), [state.items]); return ( {children} ); } function useCart() { const context = useContext(CartContext); if (!context) throw new Error('useCart must be used within CartProvider'); return context; } ``` ### Zustand (Lightweight Alternative) ```tsx import { create } from 'zustand'; import { persist } from 'zustand/middleware'; interface AuthStore { user: User | null; token: string | null; login: (email: string, password: string) => Promise; logout: () => void; } const useAuthStore = create()( persist( (set) => ({ user: null, token: null, login: async (email, password) => { const { user, token } = await authAPI.login(email, password); set({ user, token }); }, logout: () => set({ user: null, token: null }), }), { name: 'auth-storage' } ) ); // Usage function Profile() { const { user, logout } = useAuthStore(); return user ?
{user.name}
: null; } ``` --- ## Performance Patterns ### React.memo with Custom Comparison ```tsx interface ListItemProps { item: { id: string; name: string; count: number }; onSelect: (id: string) => void; } const ListItem = React.memo( function ListItem({ item, onSelect }: ListItemProps) { return (
onSelect(item.id)}> {item.name} ({item.count})
); }, (prevProps, nextProps) => { // Only re-render if item data changed return ( prevProps.item.id === nextProps.item.id && prevProps.item.name === nextProps.item.name && prevProps.item.count === nextProps.item.count ); } ); ``` ### useMemo for Expensive Calculations ```tsx function DataTable({ data, sortColumn, filterText }: { data: Item[]; sortColumn: string; filterText: string; }) { const processedData = useMemo(() => { // Filter let result = data.filter(item => item.name.toLowerCase().includes(filterText.toLowerCase()) ); // Sort result = [...result].sort((a, b) => { const aVal = a[sortColumn as keyof Item]; const bVal = b[sortColumn as keyof Item]; return aVal < bVal ? -1 : aVal > bVal ? 1 : 0; }); return result; }, [data, sortColumn, filterText]); return ( {processedData.map(item => ( {/* ... */} ))}
); } ``` ### useCallback for Stable References ```tsx function ParentComponent() { const [items, setItems] = useState([]); // Stable reference - won't cause child re-renders const handleItemClick = useCallback((id: string) => { setItems(prev => prev.map(item => item.id === id ? { ...item, selected: !item.selected } : item )); }, []); const handleAddItem = useCallback((newItem: Item) => { setItems(prev => [...prev, newItem]); }, []); return ( <> ); } ``` ### Virtualization for Long Lists ```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, // estimated row height overscan: 5, }); return (
{virtualizer.getVirtualItems().map(virtualRow => (
{items[virtualRow.index].name}
))}
); } ``` --- ## Error Boundaries ### Class-Based Error Boundary ```tsx interface ErrorBoundaryProps { children: React.ReactNode; fallback?: React.ReactNode; onError?: (error: Error, errorInfo: React.ErrorInfo) => void; } interface ErrorBoundaryState { hasError: boolean; error: Error | null; } class ErrorBoundary extends React.Component { state: ErrorBoundaryState = { hasError: false, error: null }; static getDerivedStateFromError(error: Error): ErrorBoundaryState { return { hasError: true, error }; } componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { this.props.onError?.(error, errorInfo); // Log to error reporting service console.error('Error caught:', error, errorInfo); } render() { if (this.state.hasError) { return this.props.fallback || (

Something went wrong

{this.state.error?.message}

); } return this.props.children; } } // Usage } onError={(error) => trackError(error)} > ``` ### Suspense with Error Boundary ```tsx function DataComponent() { return ( }> }> ); } ``` --- ## Anti-Patterns ### Avoid: Inline Object/Array Creation in JSX ```tsx // BAD - Creates new object every render, causes re-renders // GOOD - Define outside or use useMemo const style = { color: 'red' }; const items = [1, 2, 3]; // Or with useMemo for dynamic values const style = useMemo(() => ({ color: theme.primary }), [theme.primary]); ``` ### Avoid: Index as Key for Dynamic Lists ```tsx // BAD - Index keys break with reordering/filtering {items.map((item, index) => ( ))} // GOOD - Use stable unique ID {items.map(item => ( ))} ``` ### Avoid: Prop Drilling ```tsx // BAD - Passing props through many levels // GOOD - Use Context const UserContext = createContext(null); function App() { return ( ); } function UserInfo() { const user = useContext(UserContext); return
{user?.name}
; } ``` ### Avoid: Mutating State Directly ```tsx // BAD - Mutates state directly const addItem = (item: Item) => { items.push(item); // WRONG setItems(items); // Won't trigger re-render }; // GOOD - Create new array const addItem = (item: Item) => { setItems(prev => [...prev, item]); }; // GOOD - For objects const updateUser = (field: string, value: string) => { setUser(prev => ({ ...prev, [field]: value })); }; ``` ### Avoid: useEffect for Derived State ```tsx // BAD - Unnecessary effect and extra render const [items, setItems] = useState([]); const [total, setTotal] = useState(0); useEffect(() => { setTotal(items.reduce((sum, item) => sum + item.price, 0)); }, [items]); // GOOD - Compute during render const [items, setItems] = useState([]); const total = items.reduce((sum, item) => sum + item.price, 0); // Or useMemo for expensive calculations const total = useMemo( () => items.reduce((sum, item) => sum + item.price, 0), [items] ); ```