Advanced React patterns, hooks, state management, and performance optimization techniques for modern web applications

Modern React Patterns and Best Practices: Building Scalable Applicationsh1
Hello! I’m Ahmet Zeybek, a full stack developer with extensive experience in building complex React applications. React has evolved tremendously since its inception, and mastering modern patterns is crucial for building maintainable, performant applications. In this comprehensive guide, I’ll share advanced React patterns and best practices that I’ve refined through years of production experience.
The Evolution of React Patternsh2
React’s ecosystem has matured significantly, moving from class components to functional components, from simple hooks to sophisticated state management solutions. Let’s explore the patterns that define modern React development.
1. Compound Components Patternh3
Problem: Creating reusable component APIs that are flexible yet intuitive.
Solution: Components that work together as a cohesive unit.
// Compound Components Patterninterface TabsContextValue { activeTab: string setActiveTab: (tab: string) => void}
const TabsContext = createContext<TabsContextValue | undefined>(undefined)
interface TabsProps { defaultValue: string children: React.ReactNode}
export const Tabs: React.FC<TabsProps> & { List: typeof TabsList Trigger: typeof TabsTrigger Content: typeof TabsContent} = ({ defaultValue, children }) => { const [activeTab, setActiveTab] = useState(defaultValue)
return ( <TabsContext.Provider value={{ activeTab, setActiveTab }}> <div className="tabs">{children}</div> </TabsContext.Provider> )}
const TabsList: React.FC<{ className?: string }> = ({ className, children }) => ( <div className={`tabs-list ${className || ''}`}>{children}</div>)
const TabsTrigger: React.FC<{ value: string; className?: string }> = ({ value, className, children}) => { const { activeTab, setActiveTab } = useContext(TabsContext)!
return ( <button className={`tabs-trigger ${activeTab === value ? 'active' : ''} ${className || ''}`} onClick={() => setActiveTab(value)} > {children} </button> )}
const TabsContent: React.FC<{ value: string; className?: string }> = ({ value, className, children}) => { const { activeTab } = useContext(TabsContext)!
if (activeTab !== value) return null
return <div className={`tabs-content ${className || ''}`}>{children}</div>}
// Compound component assignmentTabs.List = TabsListTabs.Trigger = TabsTriggerTabs.Content = TabsContent
// Usage<Tabs defaultValue="profile"> <Tabs.List> <Tabs.Trigger value="profile">Profile</Tabs.Trigger> <Tabs.Trigger value="settings">Settings</Tabs.Trigger> <Tabs.Trigger value="billing">Billing</Tabs.Trigger> </Tabs.List> <Tabs.Content value="profile">Profile content...</Tabs.Content> <Tabs.Content value="settings">Settings content...</Tabs.Content> <Tabs.Content value="billing">Billing content...</Tabs.Content></Tabs>
2. Render Props Patternh3
Problem: Sharing logic between components without tight coupling.
Solution: Pass a function as a prop that renders JSX.
interface DataFetcherProps<T> { url: string render: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode}
function DataFetcher<T>({ url, render }: DataFetcherProps<T>) { const [data, setData] = useState<T | null>(null) const [loading, setLoading] = useState(true) const [error, setError] = useState<Error | null>(null)
useEffect(() => { const fetchData = async () => { try { setLoading(true) setError(null) const response = await fetch(url) const result = await response.json() setData(result) } catch (err) { setError(err as Error) } finally { setLoading(false) } }
fetchData() }, [url])
return <>{render(data, loading, error)}</>}
// Usage with render props<DataFetcher url="/api/user"> {(user, loading, error) => { if (loading) return <div>Loading...</div> if (error) return <div>Error: {error.message}</div> if (!user) return <div>No user data</div>
return ( <div> <h1>{user.name}</h1> <p>{user.email}</p> </div> ) }}</DataFetcher>
3. Higher-Order Components (HOCs)h3
Problem: Reusing component logic across multiple components.
Solution: Functions that take a component and return an enhanced component.
// HOC for adding loading statefunction withLoading<P extends object>(Component: React.ComponentType<P>) { return function WithLoadingComponent({ isLoading, ...props }: P & { isLoading?: boolean }) { if (isLoading) { return <div className="loading-spinner">Loading...</div> } return <Component {...(props as P)} /> }}
// HOC for authenticationfunction withAuth<P extends object>(Component: React.ComponentType<P>) { return function WithAuthComponent(props: P) { const { user, loading } = useAuth()
if (loading) return <div>Loading...</div> if (!user) return <LoginPrompt />
return <Component {...props} /> }}
// HOC for data fetchingfunction withData<P extends object>( dataFetcher: () => Promise<any>, dataPropName: string = 'data') { return function WithDataComponent(Component: React.ComponentType<P>) { return function WrappedComponent(props: P) { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null)
useEffect(() => { dataFetcher() .then(setData) .catch(setError) .finally(() => setLoading(false)) }, [])
return ( <Component {...props} {...{ [dataPropName]: data, loading, error }} /> ) } }}
// Usageconst UserProfileWithData = withData( () => fetch('/api/user').then(res => res.json()), 'user')(UserProfile)
const AuthenticatedUserProfile = withAuth(UserProfileWithData)
Advanced Hooks Patternsh2
4. Custom Hooks for Business Logich3
Extract complex stateful logic into reusable hooks:
// Custom hook for form handlingfunction useForm<T extends Record<string, any>>(initialValues: T) { const [values, setValues] = useState<T>(initialValues) const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({}) const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({})
const setValue = useCallback((field: keyof T, value: any) => { setValues(prev => ({ ...prev, [field]: value })) // Clear error when user starts typing if (errors[field]) { setErrors(prev => ({ ...prev, [field]: undefined })) } }, [errors])
const setTouched = useCallback((field: keyof T) => { setTouched(prev => ({ ...prev, [field]: true })) }, [])
const validate = useCallback(() => { const newErrors: Partial<Record<keyof T, string>> = {}
// Validation logic here if (!values.email) { newErrors.email = 'Email is required' }
setErrors(newErrors) return Object.keys(newErrors).length === 0 }, [values])
const reset = useCallback(() => { setValues(initialValues) setErrors({}) setTouched({}) }, [initialValues])
return { values, errors, touched, setValue, setTouched, validate, reset, isValid: Object.keys(errors).length === 0 }}
// Custom hook for API callsfunction useApi<T>(url: string, options?: RequestInit) { const [data, setData] = useState<T | null>(null) const [loading, setLoading] = useState(false) const [error, setError] = useState<Error | null>(null)
const fetchData = useCallback(async () => { try { setLoading(true) setError(null) const response = await fetch(url, options) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } const result = await response.json() setData(result) } catch (err) { setError(err as Error) } finally { setLoading(false) } }, [url, options])
useEffect(() => { fetchData() }, [fetchData])
return { data, loading, error, refetch: fetchData }}
// Usagefunction UserForm() { const { values, errors, setValue, validate, reset } = useForm({ name: '', email: '', role: 'user' })
const { data: roles, loading: rolesLoading } = useApi('/api/roles')
const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (validate()) { // Submit form console.log('Form data:', values) } }
return ( <form onSubmit={handleSubmit}> <input value={values.name} onChange={(e) => setValue('name', e.target.value)} placeholder="Name" /> {errors.name && <span className="error">{errors.name}</span>}
<input value={values.email} onChange={(e) => setValue('email', e.target.value)} placeholder="Email" /> {errors.email && <span className="error">{errors.email}</span>}
<select value={values.role} onChange={(e) => setValue('role', e.target.value)} > {rolesLoading ? ( <option>Loading roles...</option> ) : ( roles?.map((role: any) => ( <option key={role.id} value={role.name}> {role.name} </option> )) )} </select>
<button type="submit">Submit</button> <button type="button" onClick={reset}>Reset</button> </form> )}
5. State Management Patternsh3
Modern state management with React Query and Zustand:
// Zustand store for global stateimport { create } from 'zustand'import { devtools, persist } from 'zustand/middleware'
interface User { id: string name: string email: string role: string}
interface AuthState { user: User | null isAuthenticated: boolean login: (email: string, password: string) => Promise<void> logout: () => void updateProfile: (updates: Partial<User>) => Promise<void>}
export const useAuthStore = create<AuthState>()( devtools( persist( (set, get) => ({ user: null, isAuthenticated: false,
login: async (email: string, password: string) => { try { const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }) })
if (!response.ok) { throw new Error('Login failed') }
const userData = await response.json() set({ user: userData.user, isAuthenticated: true }) } catch (error) { throw error } },
logout: () => { set({ user: null, isAuthenticated: false }) },
updateProfile: async (updates: Partial<User>) => { const { user } = get() if (!user) throw new Error('No user logged in')
try { const response = await fetch(`/api/users/${user.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updates) })
if (!response.ok) { throw new Error('Update failed') }
const updatedUser = await response.json() set({ user: updatedUser }) } catch (error) { throw error } } }), { name: 'auth-storage', partialize: (state) => ({ user: state.user, isAuthenticated: state.isAuthenticated }) } ), { name: 'AuthStore' } ))
// React Query for server stateimport { QueryClient, QueryClientProvider, useQuery, useMutation } from '@tanstack/react-query'
const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 5 * 60 * 1000, // 5 minutes cacheTime: 10 * 60 * 1000, // 10 minutes retry: (failureCount, error: any) => { // Don't retry on 4xx errors if (error?.status >= 400 && error?.status < 500) { return false } return failureCount < 3 } } }})
// Custom hook for postsfunction usePosts() { return useQuery({ queryKey: ['posts'], queryFn: async () => { const response = await fetch('/api/posts') if (!response.ok) { throw new Error('Failed to fetch posts') } return response.json() } })}
// Posts component with optimistic updatesfunction PostsManager() { const { data: posts, isLoading, error } = usePosts() const createPostMutation = useMutation({ mutationFn: async (newPost: { title: string; content: string }) => { const response = await fetch('/api/posts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newPost) }) return response.json() }, onMutate: async (newPost) => { // Cancel outgoing refetches await queryClient.cancelQueries({ queryKey: ['posts'] })
// Snapshot previous value const previousPosts = queryClient.getQueryData(['posts'])
// Optimistically update cache queryClient.setQueryData(['posts'], (old: any) => ({ ...old, posts: [...old.posts, { ...newPost, id: 'temp-id', status: 'pending' }] }))
return { previousPosts } }, onError: (err, newPost, context) => { // Rollback on error if (context?.previousPosts) { queryClient.setQueryData(['posts'], context.previousPosts) } }, onSettled: () => { // Always refetch after mutation queryClient.invalidateQueries({ queryKey: ['posts'] }) } })
if (isLoading) return <div>Loading posts...</div> if (error) return <div>Error: {error.message}</div>
return ( <div> {posts?.posts.map((post: any) => ( <div key={post.id} className={post.status === 'pending' ? 'pending' : ''}> <h3>{post.title}</h3> <p>{post.content}</p> </div> ))}
<form onSubmit={(e) => { e.preventDefault() const formData = new FormData(e.currentTarget) createPostMutation.mutate({ title: formData.get('title') as string, content: formData.get('content') as string }) }}> <input name="title" placeholder="Title" /> <textarea name="content" placeholder="Content" /> <button type="submit">Create Post</button> </form> </div> )}
// App wrapperfunction App() { return ( <QueryClientProvider client={queryClient}> <PostsManager /> </QueryClientProvider> )}
Performance Optimization Patternsh2
6. Virtualization for Large Listsh3
Handle thousands of items efficiently:
import { useVirtualizer } from '@tanstack/react-virtual'
// Virtual scrolling for large listsfunction VirtualizedList({ items }: { items: any[] }) { const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 50, // Estimated height of each row overscan: 5 // Render 5 extra items outside visible area })
return ( <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}> <div style={{ height: `${virtualizer.getTotalSize()}px`, width: '100%', position: 'relative' }} > {virtualizer.getVirtualItems().map((virtualItem) => { const item = items[virtualItem.index]
return ( <div key={virtualItem.key} style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: `${virtualItem.size}px`, transform: `translateY(${virtualItem.start}px)` }} > <div className="list-item"> <h4>{item.title}</h4> <p>{item.description}</p> </div> </div> ) })} </div> </div> )}
7. Memoization Strategiesh3
Prevent unnecessary re-renders:
import { memo, useMemo, useCallback } from 'react'
// Component memoizationconst ExpensiveComponent = memo(({ data, onClick }: { data: any; onClick: (id: string) => void }) => { console.log('ExpensiveComponent rendered') return ( <div onClick={() => onClick(data.id)}> {data.title} </div> )})
// Expensive calculation memoizationfunction DataProcessor({ rawData }: { rawData: any[] }) { const processedData = useMemo(() => { console.log('Processing data...') return rawData.map(item => ({ ...item, processedAt: new Date(), hash: generateHash(item) })) }, [rawData]) // Only recalculate when rawData changes
return <div>{processedData.length} items processed</div>}
// Callback memoizationfunction ParentComponent() { const [count, setCount] = useState(0)
const handleClick = useCallback((id: string) => { console.log('Clicked item:', id) setCount(prev => prev + 1) }, []) // Empty dependency array - never changes
return ( <ChildComponent onItemClick={handleClick} /> )}
Error Boundary Patternsh2
8. Advanced Error Boundariesh3
Handle errors gracefully across component trees:
interface ErrorBoundaryState { hasError: boolean error: Error | null errorInfo: React.ErrorInfo | null}
interface ErrorBoundaryProps { fallback?: React.ComponentType<{ error: Error; resetError: () => void }> onError?: (error: Error, errorInfo: React.ErrorInfo) => void}
class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> { constructor(props: ErrorBoundaryProps) { super(props) this.state = { hasError: false, error: null, errorInfo: null } }
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> { return { hasError: true, error } }
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { this.setState({ errorInfo })
// Log error to monitoring service this.props.onError?.(error, errorInfo)
// Report to error tracking service if (window.gtag) { gtag('event', 'exception', { description: error.message, fatal: false }) } }
resetError = () => { this.setState({ hasError: false, error: null, errorInfo: null }) }
render() { if (this.state.hasError) { if (this.props.fallback) { const FallbackComponent = this.props.fallback return <FallbackComponent error={this.state.error!} resetError={this.resetError} /> }
return ( <div className="error-fallback"> <h2>Something went wrong</h2> <p>{this.state.error?.message}</p> <button onClick={this.resetError}>Try again</button> </div> ) }
return this.props.children }}
// Custom fallback componentconst ErrorFallback: React.FC<{ error: Error; resetError: () => void }> = ({ error, resetError}) => ( <div className="error-fallback"> <h2>Application Error</h2> <p>A critical error occurred: {error.message}</p> <details> <summary>Error Details</summary> <pre>{error.stack}</pre> </details> <button onClick={resetError}>Reload Application</button> </div>)
// Usage with different fallback strategiesfunction App() { return ( <ErrorBoundary onError={(error, errorInfo) => { // Log to monitoring service console.error('App Error:', error, errorInfo) }} > <ErrorBoundary fallback={ErrorFallback}> <MainApplication /> </ErrorBoundary> </ErrorBoundary> )}
Testing Patternsh2
9. Modern Testing Strategiesh3
Comprehensive testing with React Testing Library and Jest:
import { render, screen, fireEvent, waitFor } from '@testing-library/react'import userEvent from '@testing-library/user-event'import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
// Test utilitiesconst createTestQueryClient = () => new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } }})
const renderWithProviders = (ui: React.ReactElement) => { const queryClient = createTestQueryClient() return render( <QueryClientProvider client={queryClient}> {ui} </QueryClientProvider> )}
// Component testdescribe('UserForm', () => { const user = userEvent.setup()
it('should submit form with valid data', async () => { const mockSubmit = jest.fn() renderWithProviders(<UserForm onSubmit={mockSubmit} />)
// Fill form await user.type(screen.getByLabelText(/name/i), 'John Doe') await user.type(screen.getByLabelText(/email/i), 'john@example.com')
// Submit form await user.click(screen.getByRole('button', { name: /submit/i }))
// Assert await waitFor(() => { expect(mockSubmit).toHaveBeenCalledWith({ name: 'John Doe', email: 'john@example.com' }) }) })
it('should show validation errors', async () => { renderWithProviders(<UserForm />)
// Try to submit empty form await user.click(screen.getByRole('button', { name: /submit/i }))
// Check for validation errors expect(screen.getByText(/name is required/i)).toBeInTheDocument() expect(screen.getByText(/email is required/i)).toBeInTheDocument() })})
// Hook testingdescribe('useForm', () => { it('should initialize with default values', () => { const { result } = renderHook(() => useForm({ name: '', email: '' }) )
expect(result.current.values).toEqual({ name: '', email: '' }) expect(result.current.errors).toEqual({}) })
it('should validate required fields', () => { const { result } = renderHook(() => useForm({ name: '', email: '' }) )
const isValid = result.current.validate()
expect(isValid).toBe(false) expect(result.current.errors.name).toBe('Name is required') expect(result.current.errors.email).toBe('Email is required') })})
// Integration testing with mocked APIdescribe('PostsManager Integration', () => { beforeEach(() => { // Mock API responses fetchMock.mockResponse(JSON.stringify({ posts: [ { id: '1', title: 'Test Post', content: 'Test content' } ] })) })
afterEach(() => { fetchMock.resetMocks() })
it('should fetch and display posts', async () => { renderWithProviders(<PostsManager />)
// Wait for data to load await waitFor(() => { expect(screen.getByText('Test Post')).toBeInTheDocument() })
// Check loading state is removed expect(screen.queryByText(/loading/i)).not.toBeInTheDocument() })})
Accessibility Patternsh2
10. Building Accessible React Componentsh3
Ensure your components work for all users:
// Accessible form componentsconst AccessibleInput: React.FC<{ label: string name: string type?: string error?: string required?: boolean}> = ({ label, name, type = 'text', error, required, children, ...props }) => { const id = useId() const errorId = `${id}-error`
return ( <div className="form-group"> <label htmlFor={id} className="form-label"> {label} {required && <span aria-label="required">*</span>} </label> <input id={id} name={name} type={type} aria-required={required} aria-invalid={!!error} aria-describedby={error ? errorId : undefined} className={`form-input ${error ? 'error' : ''}`} {...props} /> {error && ( <div id={errorId} className="form-error" role="alert"> {error} </div> )} </div> )}
// Accessible modalconst AccessibleModal: React.FC<{ isOpen: boolean onClose: () => void title: string children: React.ReactNode}> = ({ isOpen, onClose, title, children }) => { const modalRef = useRef<HTMLDivElement>(null)
useEffect(() => { if (isOpen && modalRef.current) { // Focus management const focusableElements = modalRef.current.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ) const firstElement = focusableElements[0] as HTMLElement firstElement?.focus()
// Trap focus within modal const handleTabKey = (e: KeyboardEvent) => { if (e.key === 'Tab') { const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement if (e.shiftKey && document.activeElement === firstElement) { e.preventDefault() lastElement?.focus() } else if (!e.shiftKey && document.activeElement === lastElement) { e.preventDefault() firstElement?.focus() } } }
document.addEventListener('keydown', handleTabKey) return () => document.removeEventListener('keydown', handleTabKey) } }, [isOpen])
if (!isOpen) return null
return ( <div className="modal-overlay" onClick={onClose} role="dialog" aria-modal="true" aria-labelledby="modal-title" > <div ref={modalRef} className="modal-content" onClick={(e) => e.stopPropagation()} > <header className="modal-header"> <h2 id="modal-title">{title}</h2> <button onClick={onClose} className="modal-close" aria-label="Close modal" > × </button> </header> <main className="modal-body"> {children} </main> </div> </div> )}
// Accessible data tableconst AccessibleTable: React.FC<{ data: any[] columns: Array<{ key: string; label: string; sortable?: boolean }>}> = ({ data, columns }) => { const [sortConfig, setSortConfig] = useState<{ key: string; direction: 'asc' | 'desc' } | null>(null)
const sortedData = useMemo(() => { if (!sortConfig) return data
return [...data].sort((a, b) => { const aValue = a[sortConfig.key] const bValue = b[sortConfig.key]
if (aValue < bValue) return sortConfig.direction === 'asc' ? -1 : 1 if (aValue > bValue) return sortConfig.direction === 'asc' ? 1 : -1 return 0 }) }, [data, sortConfig])
return ( <table role="table" className="data-table"> <thead> <tr> {columns.map((column) => ( <th key={column.key} role="columnheader"> {column.sortable ? ( <button onClick={() => { setSortConfig({ key: column.key, direction: sortConfig?.key === column.key && sortConfig.direction === 'asc' ? 'desc' : 'asc' }) }} aria-sort={ sortConfig?.key === column.key ? sortConfig.direction === 'asc' ? 'ascending' : 'descending' : 'none' } className="sortable-header" > {column.label} <span className="sort-indicator" aria-hidden="true"> {sortConfig?.key === column.key ? sortConfig.direction === 'asc' ? '↑' : '↓' : '↕' } </span> </button> ) : ( column.label )} </th> ))} </tr> </thead> <tbody> {sortedData.map((row, index) => ( <tr key={row.id || index} role="row"> {columns.map((column) => ( <td key={column.key} role="cell"> {row[column.key]} </td> ))} </tr> ))} </tbody> </table> )}
Conclusionh2
Modern React development is about choosing the right patterns for your specific use case. The patterns I’ve shared here form a solid foundation for building scalable, maintainable React applications.
Key takeaways:
- Compound Components for flexible component APIs
- Custom Hooks for reusable business logic
- State Management with React Query and Zustand
- Performance Optimization with virtualization and memoization
- Error Boundaries for graceful error handling
- Accessibility as a first-class concern
Remember, patterns are tools, not rigid rules. Always measure the impact of your choices and be willing to refactor as requirements evolve.
What React patterns have worked best for your projects? Which challenges are you facing with modern React development? Share your experiences!
Further Readingh2
- React Documentation
- React Query Documentation
- Zustand Documentation
- React Testing Library
- Web Accessibility Guidelines
This post reflects my experience as of October 2025. React ecosystem evolves rapidly, so always check for the latest patterns and best practices.