Menu

Mastering React Hooks: Advanced Patterns and Best Practices
Frontend Development⭐ Featured

Mastering React Hooks: Advanced Patterns and Best Practices

Deep dive into React Hooks with advanced patterns, custom hooks, and performance optimization techniques for modern React applications.

A

Alex Chen

Author

15 min read
#React#Hooks#JavaScript

Mastering React Hooks: Advanced Patterns and Best Practices

React Hooks have revolutionized how we write React components, enabling us to use state and other React features in functional components. In this comprehensive guide, we'll explore advanced patterns, custom hooks, and performance optimization techniques.

Understanding React Hooks Fundamentals

The Hook Rules

Before diving into advanced patterns, let's review the fundamental rules:

  1. Only call Hooks at the top level - Never inside loops, conditions, or nested functions
  2. Only call Hooks from React functions - Either React function components or custom Hooks
// ❌ Wrong - conditional Hook call
function MyComponent({ condition }) {
  if (condition) {
    const [state, setState] = useState(0); // This breaks the rules!
  }
  return <div>Content</div>;
}

// ✅ Correct - Hook at top level
function MyComponent({ condition }) {
  const [state, setState] = useState(0);
  
  if (condition) {
    // Use the state here
  }
  return <div>Content</div>;
}

Advanced useState Patterns

Functional Updates

When the new state depends on the previous state, use functional updates:

function Counter() {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => {
    // ✅ Functional update ensures we get the latest state
    setCount(prevCount => prevCount + 1);
  }, []);

  const incrementBy = useCallback((amount) => {
    setCount(prevCount => prevCount + amount);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+1</button>
      <button onClick={() => incrementBy(5)}>+5</button>
    </div>
  );
}

Complex State with useReducer

For complex state logic, useReducer is often better than useState:

const initialState = {
  loading: false,
  data: null,
  error: null
};

function dataReducer(state, action) {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true, error: null };
    case 'FETCH_SUCCESS':
      return { ...state, loading: false, data: action.payload };
    case 'FETCH_ERROR':
      return { ...state, loading: false, error: action.payload };
    case 'RESET':
      return initialState;
    default:
      throw new Error(`Unknown action type: ${action.type}`);
  }
}

function DataComponent() {
  const [state, dispatch] = useReducer(dataReducer, initialState);

  const fetchData = async () => {
    dispatch({ type: 'FETCH_START' });
    try {
      const response = await fetch('/api/data');
      const data = await response.json();
      dispatch({ type: 'FETCH_SUCCESS', payload: data });
    } catch (error) {
      dispatch({ type: 'FETCH_ERROR', payload: error.message });
    }
  };

  return (
    <div>
      {state.loading && <p>Loading...</p>}
      {state.error && <p>Error: {state.error}</p>}
      {state.data && <pre>{JSON.stringify(state.data, null, 2)}</pre>}
      <button onClick={fetchData}>Fetch Data</button>
    </div>
  );
}

Custom Hooks: Reusable Logic

Data Fetching Hook

function useApi(url) {
  const [state, dispatch] = useReducer(dataReducer, initialState);

  useEffect(() => {
    let cancelled = false;

    const fetchData = async () => {
      dispatch({ type: 'FETCH_START' });
      try {
        const response = await fetch(url);
        const data = await response.json();
        
        if (!cancelled) {
          dispatch({ type: 'FETCH_SUCCESS', payload: data });
        }
      } catch (error) {
        if (!cancelled) {
          dispatch({ type: 'FETCH_ERROR', payload: error.message });
        }
      }
    };

    fetchData();

    return () => {
      cancelled = true;
    };
  }, [url]);

  const refetch = useCallback(() => {
    dispatch({ type: 'RESET' });
  }, []);

  return { ...state, refetch };
}

// Usage
function UserProfile({ userId }) {
  const { data: user, loading, error, refetch } = useApi(`/api/users/${userId}`);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <div>
      <h1>{user?.name}</h1>
      <p>{user?.email}</p>
      <button onClick={refetch}>Refresh</button>
    </div>
  );
}

Local Storage Hook

function useLocalStorage(key, initialValue) {
  // Get from local storage then parse stored json or return initialValue
  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 Settings() {
  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>
      </select>
    </div>
  );
}

Performance Optimization

useMemo for Expensive Calculations

function ExpensiveComponent({ items, filter }) {
  // Only recalculate when items or filter changes
  const filteredItems = useMemo(() => {
    console.log('Filtering items...');
    return items.filter(item => 
      item.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [items, filter]);

  const expensiveValue = useMemo(() => {
    console.log('Calculating expensive value...');
    return filteredItems.reduce((sum, item) => sum + item.value, 0);
  }, [filteredItems]);

  return (
    <div>
      <p>Total Value: {expensiveValue}</p>
      <ul>
        {filteredItems.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

useCallback for Function Memoization

function TodoList({ todos, onToggle, onDelete }) {
  const [filter, setFilter] = useState('all');

  // Memoize the filter function to prevent unnecessary re-renders
  const filteredTodos = useMemo(() => {
    switch (filter) {
      case 'active':
        return todos.filter(todo => !todo.completed);
      case 'completed':
        return todos.filter(todo => todo.completed);
      default:
        return todos;
    }
  }, [todos, filter]);

  // Memoize event handlers
  const handleToggle = useCallback((id) => {
    onToggle(id);
  }, [onToggle]);

  const handleDelete = useCallback((id) => {
    onDelete(id);
  }, [onDelete]);

  return (
    <div>
      <div>
        <button 
          onClick={() => setFilter('all')}
          className={filter === 'all' ? 'active' : ''}
        >
          All
        </button>
        <button 
          onClick={() => setFilter('active')}
          className={filter === 'active' ? 'active' : ''}
        >
          Active
        </button>
        <button 
          onClick={() => setFilter('completed')}
          className={filter === 'completed' ? 'active' : ''}
        >
          Completed
        </button>
      </div>
      
      <ul>
        {filteredTodos.map(todo => (
          <TodoItem
            key={todo.id}
            todo={todo}
            onToggle={handleToggle}
            onDelete={handleDelete}
          />
        ))}
      </ul>
    </div>
  );
}

// Memoized component to prevent unnecessary re-renders
const TodoItem = React.memo(({ todo, onToggle, onDelete }) => {
  return (
    <li>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      <span className={todo.completed ? 'completed' : ''}>
        {todo.text}
      </span>
      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </li>
  );
});

Advanced Patterns

Compound Components Pattern

function Tabs({ children, defaultTab = 0 }) {
  const [activeTab, setActiveTab] = useState(defaultTab);

  return (
    <div className="tabs">
      {React.Children.map(children, (child, index) => {
        if (React.isValidElement(child)) {
          return React.cloneElement(child, {
            isActive: index === activeTab,
            onActivate: () => setActiveTab(index),
            index
          });
        }
        return child;
      })}
    </div>
  );
}

function Tab({ children, isActive, onActivate, label }) {
  return (
    <div>
      <button 
        onClick={onActivate}
        className={isActive ? 'active' : ''}
      >
        {label}
      </button>
      {isActive && <div className="tab-content">{children}</div>}
    </div>
  );
}

// Usage
function App() {
  return (
    <Tabs defaultTab={0}>
      <Tab label="Tab 1">
        <p>Content for tab 1</p>
      </Tab>
      <Tab label="Tab 2">
        <p>Content for tab 2</p>
      </Tab>
      <Tab label="Tab 3">
        <p>Content for tab 3</p>
      </Tab>
    </Tabs>
  );
}

Context + Hooks Pattern

const ThemeContext = createContext();

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  const toggleTheme = useCallback(() => {
    setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
  }, []);

  const value = useMemo(() => ({
    theme,
    toggleTheme,
    colors: theme === 'light' ? lightColors : darkColors
  }), [theme, toggleTheme]);

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

// Usage
function ThemedButton() {
  const { theme, toggleTheme, colors } = useTheme();
  
  return (
    <button 
      onClick={toggleTheme}
      style={{ 
        backgroundColor: colors.primary,
        color: colors.text 
      }}
    >
      Current theme: {theme}
    </button>
  );
}

Testing Hooks

Testing Custom Hooks

import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

describe('useCounter', () => {
  it('should initialize with default value', () => {
    const { result } = renderHook(() => useCounter());
    expect(result.current.count).toBe(0);
  });

  it('should initialize with provided value', () => {
    const { result } = renderHook(() => useCounter(10));
    expect(result.current.count).toBe(10);
  });

  it('should increment count', () => {
    const { result } = renderHook(() => useCounter());
    
    act(() => {
      result.current.increment();
    });
    
    expect(result.current.count).toBe(1);
  });

  it('should decrement count', () => {
    const { result } = renderHook(() => useCounter(5));
    
    act(() => {
      result.current.decrement();
    });
    
    expect(result.current.count).toBe(4);
  });
});

Best Practices Summary

  1. Follow the Rules of Hooks - Always call Hooks at the top level
  2. Use the Right Hook - useState for simple state, useReducer for complex state
  3. Optimize Performance - Use useMemo and useCallback judiciously
  4. Create Custom Hooks - Extract reusable logic into custom Hooks
  5. Handle Cleanup - Always clean up subscriptions and async operations
  6. Test Your Hooks - Write tests for custom Hooks and complex logic
  7. Use TypeScript - Add type safety to your Hooks

Conclusion

React Hooks provide a powerful way to manage state and side effects in functional components. By mastering these advanced patterns and following best practices, you can write more maintainable, performant, and reusable React code.

The key is to understand when and how to use each Hook effectively, create custom Hooks for reusable logic, and always keep performance and testing in mind.