React Component Patterns

React Component Patterns: Modern Development Guide

Table of Contents

  1. Introduction to React Components
  2. Functional vs Class Components
  3. Component Composition Patterns
  4. State Management Patterns
  5. Higher-Order Components (HOCs)
  6. Render Props Pattern
  7. Custom Hooks
  8. Context and Provider Patterns
  9. Performance Optimization Patterns
  10. 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

  1. Presentational Components: Focus on UI rendering
  2. Container Components: Handle business logic and state
  3. Layout Components: Structure and positioning
  4. 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.