React Component Patterns: Modern Development Guide
Table of Contents
- Introduction to React Components
- Functional vs Class Components
- Component Composition Patterns
- State Management Patterns
- Higher-Order Components (HOCs)
- Render Props Pattern
- Custom Hooks
- Context and Provider Patterns
- Performance Optimization Patterns
- Testing Patterns
Introduction to React Components {#introduction}
React components are the building blocks of React applications. They encapsulate UI logic and state, promoting reusability and maintainability. Understanding component patterns is crucial for building scalable React applications.
Component Fundamentals
// Basic functional component
function Welcome({ name, age }) {
return (
<div className="welcome">
<h1>Hello, {name}!</h1>
<p>You are {age} years old.</p>
</div>
);
}
// Component with default props
Welcome.defaultProps = {
name: 'Guest',
age: 0
};
// Usage
<Welcome name="Alice" age={25} />
Component Types by Purpose
- Presentational Components: Focus on UI rendering
- Container Components: Handle business logic and state
- Layout Components: Structure and positioning
- Utility Components: Shared functionality
Functional vs Class Components {#functional-vs-class}
Modern Functional Components with Hooks
import React, { useState, useEffect, useCallback, useMemo } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Effect for data fetching
useEffect(() => {
let cancelled = false;
async function fetchUser() {
try {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
if (!cancelled) {
setUser(userData);
setError(null);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchUser();
return () => {
cancelled = true;
};
}, [userId]);
// Memoized computed value
const userDisplayName = useMemo(() => {
if (!user) return '';
return `${user.firstName} ${user.lastName}`.trim();
}, [user]);
// Memoized event handler
const handleRefresh = useCallback(() => {
setUser(null);
setLoading(true);
setError(null);
}, []);
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage message={error} onRetry={handleRefresh} />;
if (!user) return <div>User not found</div>;
return (
<div className="user-profile">
<Avatar src={user.avatar} alt={userDisplayName} />
<h2>{userDisplayName}</h2>
<p>{user.email}</p>
<button onClick={handleRefresh}>Refresh</button>
</div>
);
}
Legacy Class Components (for reference)
class UserProfileClass extends React.Component {
constructor(props) {
super(props);
this.state = {
user: null,
loading: true,
error: null
};
this.handleRefresh = this.handleRefresh.bind(this);
}
async componentDidMount() {
await this.fetchUser();
}
async componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
await this.fetchUser();
}
}
async fetchUser() {
try {
this.setState({ loading: true });
const response = await fetch(`/api/users/${this.props.userId}`);
const user = await response.json();
this.setState({ user, error: null });
} catch (error) {
this.setState({ error: error.message });
} finally {
this.setState({ loading: false });
}
}
handleRefresh() {
this.setState({ user: null, loading: true, error: null });
this.fetchUser();
}
render() {
const { user, loading, error } = this.state;
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage message={error} onRetry={this.handleRefresh} />;
if (!user) return <div>User not found</div>;
return (
<div className="user-profile">
<Avatar src={user.avatar} alt={`${user.firstName} ${user.lastName}`} />
<h2>{user.firstName} {user.lastName}</h2>
<p>{user.email}</p>
<button onClick={this.handleRefresh}>Refresh</button>
</div>
);
}
}
Component Composition Patterns {#composition}
Children Pattern
function Card({ children, title, className = '' }) {
return (
<div className={`card ${className}`}>
{title && <div className="card-header">{title}</div>}
<div className="card-body">
{children}
</div>
</div>
);
}
// Usage
<Card title="User Information">
<UserProfile userId={123} />
<UserActions userId={123} />
</Card>
Compound Components Pattern
function Tabs({ children, defaultTab = 0 }) {
const [activeTab, setActiveTab] = useState(defaultTab);
const tabs = React.Children.toArray(children);
return (
<div className="tabs">
<div className="tab-list">
{tabs.map((tab, index) => (
<button
key={index}
className={`tab ${index === activeTab ? 'active' : ''}`}
onClick={() => setActiveTab(index)}
>
{tab.props.label}
</button>
))}
</div>
<div className="tab-content">
{tabs[activeTab]}
</div>
</div>
);
}
function TabPane({ children, label }) {
return <div className="tab-pane">{children}</div>;
}
// Usage
<Tabs defaultTab={1}>
<TabPane label="Profile">
<UserProfile />
</TabPane>
<TabPane label="Settings">
<UserSettings />
</TabPane>
<TabPane label="Activity">
<UserActivity />
</TabPane>
</Tabs>
Slot Pattern
function Layout({ header, sidebar, main, footer }) {
return (
<div className="layout">
<header className="layout-header">{header}</header>
<div className="layout-body">
<aside className="layout-sidebar">{sidebar}</aside>
<main className="layout-main">{main}</main>
</div>
<footer className="layout-footer">{footer}</footer>
</div>
);
}
// Usage
<Layout
header={<Navigation />}
sidebar={<SidebarMenu />}
main={<MainContent />}
footer={<Footer />}
/>
State Management Patterns {#state-management}
Local State with useState
function Counter({ initialValue = 0, step = 1 }) {
const [count, setCount] = useState(initialValue);
const increment = useCallback(() => {
setCount(prevCount => prevCount + step);
}, [step]);
const decrement = useCallback(() => {
setCount(prevCount => prevCount - step);
}, [step]);
const reset = useCallback(() => {
setCount(initialValue);
}, [initialValue]);
return (
<div className="counter">
<span className="count">{count}</span>
<div className="controls">
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
<button onClick={increment}>+</button>
</div>
</div>
);
}
State with useReducer
function formReducer(state, action) {
switch (action.type) {
case 'SET_FIELD':
return {
...state,
values: {
...state.values,
[action.field]: action.value
},
errors: {
...state.errors,
[action.field]: null
}
};
case 'SET_ERROR':
return {
...state,
errors: {
...state.errors,
[action.field]: action.error
}
};
case 'SET_LOADING':
return {
...state,
loading: action.loading
};
case 'RESET_FORM':
return {
values: action.initialValues || {},
errors: {},
loading: false
};
default:
return state;
}
}
function ContactForm({ onSubmit, initialValues = {} }) {
const [state, dispatch] = useReducer(formReducer, {
values: initialValues,
errors: {},
loading: false
});
const setField = useCallback((field, value) => {
dispatch({ type: 'SET_FIELD', field, value });
}, []);
const setError = useCallback((field, error) => {
dispatch({ type: 'SET_ERROR', field, error });
}, []);
const validateField = useCallback((field, value) => {
switch (field) {
case 'email':
if (!value.includes('@')) {
setError(field, 'Invalid email address');
return false;
}
break;
case 'name':
if (value.length < 2) {
setError(field, 'Name must be at least 2 characters');
return false;
}
break;
default:
break;
}
return true;
}, [setError]);
const handleSubmit = useCallback(async (e) => {
e.preventDefault();
// Validate all fields
const isValid = Object.keys(state.values).every(field =>
validateField(field, state.values[field])
);
if (!isValid) return;
dispatch({ type: 'SET_LOADING', loading: true });
try {
await onSubmit(state.values);
dispatch({ type: 'RESET_FORM', initialValues });
} catch (error) {
setError('general', error.message);
} finally {
dispatch({ type: 'SET_LOADING', loading: false });
}
}, [state.values, validateField, onSubmit, initialValues, setError]);
return (
<form onSubmit={handleSubmit} className="contact-form">
<div className="form-group">
<label htmlFor="name">Name</label>
<input
id="name"
type="text"
value={state.values.name || ''}
onChange={(e) => setField('name', e.target.value)}
onBlur={(e) => validateField('name', e.target.value)}
/>
{state.errors.name && <span className="error">{state.errors.name}</span>}
</div>
<div className="form-group">
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={state.values.email || ''}
onChange={(e) => setField('email', e.target.value)}
onBlur={(e) => validateField('email', e.target.value)}
/>
{state.errors.email && <span className="error">{state.errors.email}</span>}
</div>
{state.errors.general && (
<div className="error general-error">{state.errors.general}</div>
)}
<button type="submit" disabled={state.loading}>
{state.loading ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}
Higher-Order Components (HOCs) {#hocs}
Authentication HOC
function withAuth(WrappedComponent, requiredRole = null) {
function AuthenticatedComponent(props) {
const { user, loading } = useAuth();
if (loading) {
return <LoadingSpinner />;
}
if (!user) {
return <LoginPrompt />;
}
if (requiredRole && !user.roles.includes(requiredRole)) {
return <AccessDenied />;
}
return <WrappedComponent {...props} user={user} />;
}
AuthenticatedComponent.displayName = `withAuth(${WrappedComponent.displayName || WrappedComponent.name})`;
return AuthenticatedComponent;
}
// Usage
const ProtectedDashboard = withAuth(Dashboard, 'admin');
const UserProfile = withAuth(Profile);
Loading HOC
function withLoading(WrappedComponent) {
function LoadingComponent({ isLoading, loadingMessage = 'Loading...', ...props }) {
if (isLoading) {
return (
<div className="loading-container">
<LoadingSpinner />
<p>{loadingMessage}</p>
</div>
);
}
return <WrappedComponent {...props} />;
}
LoadingComponent.displayName = `withLoading(${WrappedComponent.displayName || WrappedComponent.name})`;
return LoadingComponent;
}
// Usage
const UserListWithLoading = withLoading(UserList);
<UserListWithLoading
isLoading={loading}
loadingMessage="Fetching users..."
users={users}
/>
Render Props Pattern {#render-props}
Data Fetcher Component
function DataFetcher({ url, children, render }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchData() {
try {
setLoading(true);
const response = await fetch(url);
const result = await response.json();
if (!cancelled) {
setData(result);
setError(null);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchData();
return () => {
cancelled = true;
};
}, [url]);
const renderProps = { data, loading, error };
// Support both render prop and children function
if (render) {
return render(renderProps);
}
if (typeof children === 'function') {
return children(renderProps);
}
return null;
}
// Usage with render prop
<DataFetcher
url="/api/users"
render={({ data, loading, error }) => {
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage message={error} />;
return <UserList users={data} />;
}}
/>
// Usage with children function
<DataFetcher url="/api/posts">
{({ data, loading, error }) => {
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage message={error} />;
return <PostList posts={data} />;
}}
</DataFetcher>
Form Validation Render Prop
function FormValidator({ validationRules, children }) {
const [values, setValues] = useState({});
const [errors, setErrors] = useState({});
const validate = useCallback((field, value) => {
const rule = validationRules[field];
if (!rule) return true;
const error = rule(value, values);
setErrors(prev => ({ ...prev, [field]: error }));
return !error;
}, [validationRules, values]);
const setValue = useCallback((field, value) => {
setValues(prev => ({ ...prev, [field]: value }));
validate(field, value);
}, [validate]);
const validateAll = useCallback(() => {
const newErrors = {};
let isValid = true;
Object.keys(validationRules).forEach(field => {
const error = validationRules[field](values[field], values);
if (error) {
newErrors[field] = error;
isValid = false;
}
});
setErrors(newErrors);
return isValid;
}, [validationRules, values]);
return children({
values,
errors,
setValue,
validate,
validateAll,
isValid: Object.keys(errors).length === 0
});
}
// Usage
const validationRules = {
email: (value) => {
if (!value) return 'Email is required';
if (!value.includes('@')) return 'Invalid email format';
return null;
},
password: (value) => {
if (!value) return 'Password is required';
if (value.length < 8) return 'Password must be at least 8 characters';
return null;
}
};
<FormValidator validationRules={validationRules}>
{({ values, errors, setValue, validateAll, isValid }) => (
<form onSubmit={(e) => {
e.preventDefault();
if (validateAll()) {
onSubmit(values);
}
}}>
<input
type="email"
value={values.email || ''}
onChange={(e) => setValue('email', e.target.value)}
placeholder="Email"
/>
{errors.email && <span className="error">{errors.email}</span>}
<input
type="password"
value={values.password || ''}
onChange={(e) => setValue('password', e.target.value)}
placeholder="Password"
/>
{errors.password && <span className="error">{errors.password}</span>}
<button type="submit" disabled={!isValid}>Submit</button>
</form>
)}
</FormValidator>
Custom Hooks {#custom-hooks}
useLocalStorage Hook
function useLocalStorage(key, initialValue) {
// Get value from localStorage or use initial value
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});
// Return a wrapped version of useState's setter function that persists the new value to localStorage
const setValue = useCallback((value) => {
try {
// Allow value to be a function so we have the same API as useState
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
}, [key, storedValue]);
return [storedValue, setValue];
}
// Usage
function UserPreferences() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [language, setLanguage] = useLocalStorage('language', 'en');
return (
<div>
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<select value={language} onChange={(e) => setLanguage(e.target.value)}>
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
</select>
</div>
);
}
useDebounce Hook
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// Usage in search component
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 300);
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (debouncedSearchTerm) {
setLoading(true);
searchAPI(debouncedSearchTerm).then(results => {
setResults(results);
setLoading(false);
});
} else {
setResults([]);
}
}, [debouncedSearchTerm]);
return (
<div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
{loading && <div>Searching...</div>}
<SearchResults results={results} />
</div>
);
}
useApi Hook
function useApi(url, options = {}) {
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, {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, [url, options]);
useEffect(() => {
fetchData();
}, [fetchData]);
const refetch = useCallback(() => {
fetchData();
}, [fetchData]);
return { data, loading, error, refetch };
}
// Usage
function UserProfile({ userId }) {
const { data: user, loading, error, refetch } = useApi(`/api/users/${userId}`);
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage message={error} onRetry={refetch} />;
if (!user) return <div>User not found</div>;
return (
<div className="user-profile">
<h2>{user.name}</h2>
<p>{user.email}</p>
<button onClick={refetch}>Refresh</button>
</div>
);
}
Context and Provider Patterns {#context}
Theme Context
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const toggleTheme = useCallback(() => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
}, [setTheme]);
const value = useMemo(() => ({
theme,
toggleTheme,
isDark: theme === 'dark'
}), [theme, toggleTheme]);
return (
<ThemeContext.Provider value={value}>
<div className={`app-theme-${theme}`}>
{children}
</div>
</ThemeContext.Provider>
);
}
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
// Usage
function App() {
return (
<ThemeProvider>
<Header />
<Main />
<Footer />
</ThemeProvider>
);
}
function Header() {
const { theme, toggleTheme } = useTheme();
return (
<header>
<h1>My App</h1>
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'dark' : 'light'} mode
</button>
</header>
);
}
Authentication Context
const AuthContext = createContext();
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check for existing session
const token = localStorage.getItem('authToken');
if (token) {
fetchUserProfile(token).then(user => {
setUser(user);
setLoading(false);
}).catch(() => {
localStorage.removeItem('authToken');
setLoading(false);
});
} else {
setLoading(false);
}
}, []);
const login = useCallback(async (email, password) => {
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await response.json();
if (response.ok) {
localStorage.setItem('authToken', data.token);
setUser(data.user);
return { success: true };
} else {
return { success: false, error: data.message };
}
} catch (error) {
return { success: false, error: error.message };
}
}, []);
const logout = useCallback(() => {
localStorage.removeItem('authToken');
setUser(null);
}, []);
const value = useMemo(() => ({
user,
login,
logout,
isAuthenticated: !!user,
loading
}), [user, login, logout, loading]);
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
Performance Optimization Patterns {#performance}
Memoization with React.memo
const UserCard = React.memo(function UserCard({ user, onSelect }) {
return (
<div className="user-card" onClick={() => onSelect(user.id)}>
<img src={user.avatar} alt={user.name} />
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
}, (prevProps, nextProps) => {
// Custom comparison function
return (
prevProps.user.id === nextProps.user.id &&
prevProps.user.name === nextProps.user.name &&
prevProps.user.email === nextProps.user.email &&
prevProps.user.avatar === nextProps.user.avatar &&
prevProps.onSelect === nextProps.onSelect
);
});
function UserList({ users }) {
const [selectedUser, setSelectedUser] = useState(null);
// Memoize the callback to prevent unnecessary re-renders
const handleUserSelect = useCallback((userId) => {
setSelectedUser(userId);
}, []);
return (
<div className="user-list">
{users.map(user => (
<UserCard
key={user.id}
user={user}
onSelect={handleUserSelect}
/>
))}
</div>
);
}
Virtual Scrolling
function VirtualList({ items, itemHeight, containerHeight, renderItem }) {
const [scrollTop, setScrollTop] = useState(0);
const visibleStart = Math.floor(scrollTop / itemHeight);
const visibleEnd = Math.min(
visibleStart + Math.ceil(containerHeight / itemHeight) + 1,
items.length
);
const visibleItems = items.slice(visibleStart, visibleEnd);
const totalHeight = items.length * itemHeight;
const offsetY = visibleStart * itemHeight;
const handleScroll = useCallback((e) => {
setScrollTop(e.target.scrollTop);
}, []);
return (
<div
style={{ height: containerHeight, overflow: 'auto' }}
onScroll={handleScroll}
>
<div style={{ height: totalHeight, position: 'relative' }}>
<div style={{ transform: `translateY(${offsetY}px)` }}>
{visibleItems.map((item, index) => (
<div
key={visibleStart + index}
style={{ height: itemHeight }}
>
{renderItem(item, visibleStart + index)}
</div>
))}
</div>
</div>
</div>
);
}
// Usage
function App() {
const items = Array.from({ length: 10000 }, (_, i) => ({ id: i, name: `Item ${i}` }));
return (
<VirtualList
items={items}
itemHeight={50}
containerHeight={400}
renderItem={(item) => (
<div className="list-item">
{item.name}
</div>
)}
/>
);
}
Testing Patterns {#testing}
Component Testing with React Testing Library
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserProfile } from './UserProfile';
// Mock API calls
jest.mock('../api/userApi', () => ({
fetchUser: jest.fn(),
updateUser: jest.fn()
}));
describe('UserProfile', () => {
const mockUser = {
id: 1,
name: 'John Doe',
email: 'john@example.com',
avatar: 'avatar.jpg'
};
beforeEach(() => {
jest.clearAllMocks();
});
test('displays user information correctly', async () => {
fetchUser.mockResolvedValue(mockUser);
render(<UserProfile userId={1} />);
// Check loading state
expect(screen.getByText('Loading...')).toBeInTheDocument();
// Wait for user data to load
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
expect(screen.getByText('john@example.com')).toBeInTheDocument();
expect(screen.getByAltText('John Doe')).toHaveAttribute('src', 'avatar.jpg');
});
test('handles edit mode correctly', async () => {
fetchUser.mockResolvedValue(mockUser);
updateUser.mockResolvedValue({ ...mockUser, name: 'Jane Doe' });
render(<UserProfile userId={1} />);
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
// Enter edit mode
fireEvent.click(screen.getByText('Edit'));
// Check if form is displayed
expect(screen.getByDisplayValue('John Doe')).toBeInTheDocument();
expect(screen.getByDisplayValue('john@example.com')).toBeInTheDocument();
// Update name
const nameInput = screen.getByDisplayValue('John Doe');
await userEvent.clear(nameInput);
await userEvent.type(nameInput, 'Jane Doe');
// Save changes
fireEvent.click(screen.getByText('Save'));
// Verify API call
await waitFor(() => {
expect(updateUser).toHaveBeenCalledWith(1, {
...mockUser,
name: 'Jane Doe'
});
});
});
test('displays error message on fetch failure', async () => {
fetchUser.mockRejectedValue(new Error('Network error'));
render(<UserProfile userId={1} />);
await waitFor(() => {
expect(screen.getByText('Error loading user data')).toBeInTheDocument();
});
// Test retry functionality
fireEvent.click(screen.getByText('Retry'));
expect(fetchUser).toHaveBeenCalledTimes(2);
});
});
Custom Hook Testing
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
test('initializes with default value', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
test('initializes with custom value', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
test('increments count', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
test('decrements count', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(4);
});
test('resets count', () => {
const { result } = renderHook(() => useCounter(10));
act(() => {
result.current.increment();
result.current.increment();
});
expect(result.current.count).toBe(12);
act(() => {
result.current.reset();
});
expect(result.current.count).toBe(10);
});
});
This comprehensive guide covers the most important React component patterns used in modern React development. Understanding and applying these patterns will help you build more maintainable, performant, and testable React applications.
Each pattern serves a specific purpose and can be combined with others to create robust component architectures. The key is to choose the right pattern for your specific use case and maintain consistency throughout your application.