Simplifying Toggle States in React: A Custom Hook Approach
ReactCoding

Simplifying Toggle States in React: A Custom Hook Approach

Ihor ChyshkalaIhor Chyshkala

In React applications, we often find ourselves implementing various toggle states - dark/light themes, online/offline status, modal visibility, or menu states. While useState is the go-to solution for managing state, repeatedly writing similar toggle logic across components can lead to unnecessary boilerplate code. Let's explore how we can streamline this common pattern using a custom hook.

The Common Pattern

You might have seen (or written) code that looks like this:

function Navbar() {
  const [isDarkMode, setIsDarkMode] = useState(false);
  const [isMenuOpen, setIsMenuOpen] = useState(false);
  
  const toggleDarkMode = () => setIsDarkMode(prev => !prev);
  const toggleMenu = () => setIsMenuOpen(prev => !prev);
  
  return (
    <nav>
      <button onClick={toggleDarkMode}>
        {isDarkMode ? '🌙' : '☀️'}
      </button>
      <button onClick={toggleMenu}>
        {isMenuOpen ? '✕' : '☰'}
      </button>
    </nav>
  );
}

This pattern repeats across components, leading to:

  1. Redundant state declarations
  2. Duplicate toggle logic
  3. Increased possibility for errors
  4. Less maintainable code

Enter useToggleState

Let's introduce a custom hook that encapsulates this toggle behavior:

import { useState, useCallback } from 'react';

export function useToggleState(initialState: boolean) {
    const [state, setState] = useState(initialState);
    const toggle = useCallback(() => {
        setState((prev) => !prev);
    }, []);

    return [state, toggle] as const;
}

This simple yet powerful hook provides several benefits:

  • Encapsulates toggle logic in one reusable place
  • Returns a tuple with the current state and a toggle function
  • Uses useCallback to maintain reference stability
  • TypeScript support out of the box

Practical Usage Examples

Let's see how this hook simplifies our components:

function EnhancedNavbar() {
  const [isDarkMode, toggleDarkMode] = useToggleState(false);
  const [isMenuOpen, toggleMenu] = useToggleState(false);
  
  return (
    <nav>
      <button onClick={toggleDarkMode}>
        {isDarkMode ? '🌙' : '☀️'}
      </button>
      <button onClick={toggleMenu}>
        {isMenuOpen ? '✕' : '☰'}
      </button>
    </nav>
  );
}

function OnlineStatus() {
  const [isOnline, toggleOnline] = useToggleState(true);
  
  return (
    <div className={isOnline ? 'status-online' : 'status-offline'}>
      {isOnline ? 'Connected' : 'Offline'}
    </div>
  );
}

function Modal() {
  const [isVisible, toggleVisibility] = useToggleState(false);
  
  return (
    <>
      <button onClick={toggleVisibility}>
        {isVisible ? 'Hide Modal' : 'Show Modal'}
      </button>
      {isVisible && <div className="modal">Modal Content</div>}
    </>
  );
}

Advanced Usage: Adding Features

We can extend our hook to support more features while maintaining its simplicity:

interface ToggleOptions {
  onToggle?: (newState: boolean) => void;
  disabled?: boolean;
}

function useToggleState(initialState: boolean, options?: ToggleOptions) {
  const [state, setState] = useState(initialState);
  
  const toggle = useCallback(() => {
    if (options?.disabled) return;
    
    setState(prev => {
      const newState = !prev;
      options?.onToggle?.(newState);
      return newState;
    });
  }, [options?.disabled, options?.onToggle]);
  
  return [state, toggle] as const;
}

// Usage with callbacks
function ThemeToggle() {
  const [isDarkMode, toggleTheme] = useToggleState(false, {
    onToggle: (newState) => {
      document.body.classList.toggle('dark-mode', newState);
    },
  });
  
  return (
    <button onClick={toggleTheme}>
      Toggle Theme ({isDarkMode ? 'Dark' : 'Light'})
    </button>
  );
}

Best Practices and Considerations

When using useToggleState, keep in mind:

  1. Initial State: Choose meaningful default values that match your UI's initial state
  2. Performance: The hook uses useCallback to prevent unnecessary rerenders
  3. Type Safety: TypeScript support ensures correct usage across your application
  4. Naming: Use clear, descriptive names for your state variables (e.g., isVisible instead of just state)

Conclusion

Custom hooks like useToggleState demonstrate the power of React's composition model. By extracting common patterns into reusable hooks, we can:

  • Reduce boilerplate code
  • Improve code maintainability
  • Ensure consistent behavior across components
  • Make our components more focused and readable

Remember, while this hook is simple, it represents a broader pattern of identifying repeated logic in your React applications and extracting it into reusable hooks. This approach not only makes your code more maintainable but also helps establish consistent patterns across your team's codebase.

About the Author

Ihor Chyshkala

Ihor Chyshkala

Code Alchemist: Transmuting Ideas into Reality with JS & PHP. DevOps Wizard: Transforming Infrastructure into Cloud Gold | Orchestrating CI/CD Magic | Crafting Automation Elixirs