AdSense Leaderboard

Google AdSense Placeholder

HEADER SLOT

useToggle Hook: Boolean State Simplification in React

Last updated:
useToggle Hook: Boolean State Simplification in React

Master boolean state management with useToggle. Simplify on/off states, build toggle switches, and manage feature flags with clean, production-ready code patterns.

# useToggle Hook: Boolean State Simplification in React

Boolean state appears everywhere in React—visible or hidden, enabled or disabled, dark mode or light mode, menu open or closed. The standard useState(false) works, but it creates repetitive boilerplate: defining state, remembering to negate the current value, and writing setter logic. The useToggle hook collapses this pattern into a single line, turning verbose state management into clean, intention-revealing code.

While simple on the surface, a well-designed useToggle hook teaches you how React's hooks handle state, effects, and callbacks. It's an excellent foundation for understanding more complex hooks and a practical utility you'll use in nearly every component you build.

# Table of Contents

  1. Why Boolean State Needs Simplification
  2. Basic useToggle Implementation
  3. Advanced Patterns: Callbacks and Initial Values
  4. Composition with Other Hooks
  5. Practical Application Scenarios
  6. Performance Considerations
  7. FAQ

# Why Boolean State Needs Simplification

Consider a simple dark mode toggle:

javascript
// ❌ Repetitive: What should be simple is verbose
function DarkModeToggle() {
  const [isDark, setIsDark] = useState(false);

  return (
    <button onClick={() => setIsDark(!isDark)}>
      {isDark ? '☀️ Light' : '🌙 Dark'}
    </button>
  );
}

This is fine for a single toggle, but when you have multiple toggles, the pattern repeats:

javascript
// ❌ Pattern repetition becomes obvious
function UserSettings() {
  const [isDark, setIsDark] = useState(false);
  const [isNotified, setIsNotified] = useState(true);
  const [isPublic, setIsPublic] = useState(false);

  return (
    <>
      <label>
        Dark mode:
        <input
          type="checkbox"
          checked={isDark}
          onChange={() => setIsDark(!isDark)}
        />
      </label>

      <label>
        Notifications:
        <input
          type="checkbox"
          checked={isNotified}
          onChange={() => setIsNotified(!isNotified)}
        />
      </label>

      <label>
        Public profile:
        <input
          type="checkbox"
          checked={isPublic}
          onChange={() => setIsPublic(!isPublic)}
        />
      </label>
    </>
  );
}

The pattern is clear: set state, negate current value, repeat. A useToggle hook eliminates this boilerplate while making the intent explicit.

# Basic useToggle Implementation

# TypeScript Version

typescript
import { useState, useCallback } from 'react';

interface UseToggleReturn {
  // Current boolean value
  value: boolean;
  // Toggle to opposite value
  toggle: () => void;
  // Set to specific value
  setValue: (value: boolean) => void;
  // Set to true
  on: () => void;
  // Set to false
  off: () => void;
}

export function useToggle(initialValue: boolean = false): UseToggleReturn {
  const [value, setValue] = useState(initialValue);

  // Toggle to opposite value
  const toggle = useCallback(() => {
    setValue(prev => !prev);
  }, []);

  // Set to true
  const on = useCallback(() => {
    setValue(true);
  }, []);

  // Set to false
  const off = useCallback(() => {
    setValue(false);
  }, []);

  return {
    value,
    toggle,
    setValue,
    on,
    off,
  };
}

# JavaScript Version

javascript
export function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);

  const toggle = useCallback(() => {
    setValue(prev => !prev);
  }, []);

  const on = useCallback(() => {
    setValue(true);
  }, []);

  const off = useCallback(() => {
    setValue(false);
  }, []);

  return {
    value,
    toggle,
    setValue,
    on,
    off,
  };
}

# Usage Example

Now the repetitive boilerplate disappears:

typescript
// ✅ Clean and intention-revealing
function UserSettings() {
  const darkMode = useToggle(false);
  const notifications = useToggle(true);
  const publicProfile = useToggle(false);

  return (
    <>
      <label>
        Dark mode:
        <input
          type="checkbox"
          checked={darkMode.value}
          onChange={darkMode.toggle}
        />
      </label>

      <label>
        Notifications:
        <input
          type="checkbox"
          checked={notifications.value}
          onChange={notifications.toggle}
        />
      </label>

      <label>
        Public profile:
        <input
          type="checkbox"
          checked={publicProfile.value}
          onChange={publicProfile.toggle}
        />
      </label>
    </>
  );
}

# Advanced Patterns: Callbacks and Initial Values

# Toggle with Lifecycle Callbacks

typescript
interface UseToggleAdvancedOptions {
  initialValue?: boolean;
  // Called when toggled on
  onOn?: () => void;
  // Called when toggled off
  onOff?: () => void;
  // Called on any change
  onChange?: (value: boolean) => void;
}

export function useToggleAdvanced(
  options: UseToggleAdvancedOptions = {}
): UseToggleReturn {
  const {
    initialValue = false,
    onOn,
    onOff,
    onChange,
  } = options;

  const [value, setValue] = useState(initialValue);

  const toggle = useCallback(() => {
    setValue(prev => {
      const newValue = !prev;

      // Call appropriate callback
      if (newValue) {
        onOn?.();
      } else {
        onOff?.();
      }

      onChange?.(newValue);
      return newValue;
    });
  }, [onOn, onOff, onChange]);

  const on = useCallback(() => {
    setValue(prev => {
      if (prev) return prev; // Already on

      onOn?.();
      onChange?.(true);
      return true;
    });
  }, [onOn, onChange]);

  const off = useCallback(() => {
    setValue(prev => {
      if (!prev) return prev; // Already off

      onOff?.();
      onChange?.(false);
      return false;
    });
  }, [onOff, onChange]);

  const setValue_direct = useCallback((newValue: boolean) => {
    setValue(prev => {
      if (prev === newValue) return prev; // No change

      if (newValue) {
        onOn?.();
      } else {
        onOff?.();
      }

      onChange?.(newValue);
      return newValue;
    });
  }, [onOn, onOff, onChange]);

  return {
    value,
    toggle,
    setValue: setValue_direct,
    on,
    off,
  };
}

# JavaScript Version

javascript
export function useToggleAdvanced(options = {}) {
  const {
    initialValue = false,
    onOn,
    onOff,
    onChange,
  } = options;

  const [value, setValue] = useState(initialValue);

  const toggle = useCallback(() => {
    setValue(prev => {
      const newValue = !prev;

      if (newValue) {
        onOn?.();
      } else {
        onOff?.();
      }

      onChange?.(newValue);
      return newValue;
    });
  }, [onOn, onOff, onChange]);

  const on = useCallback(() => {
    setValue(prev => {
      if (prev) return prev;

      onOn?.();
      onChange?.(true);
      return true;
    });
  }, [onOn, onChange]);

  const off = useCallback(() => {
    setValue(prev => {
      if (!prev) return prev;

      onOff?.();
      onChange?.(false);
      return false;
    });
  }, [onOff, onChange]);

  const setValue_direct = useCallback((newValue) => {
    setValue(prev => {
      if (prev === newValue) return prev;

      if (newValue) {
        onOn?.();
      } else {
        onOff?.();
      }

      onChange?.(newValue);
      return newValue;
    });
  }, [onOn, onOff, onChange]);

  return {
    value,
    toggle,
    setValue: setValue_direct,
    on,
    off,
  };
}

# Composition with Other Hooks

# Toggle with useEffect

Control toggle state based on external conditions:

typescript
export function useToggleWithEffect(
  initialValue: boolean = false,
  shouldReset: boolean = false
) {
  const toggle = useToggle(initialValue);

  useEffect(() => {
    if (shouldReset) {
      toggle.off();
    }
  }, [shouldReset, toggle]);

  return toggle;
}

// Usage: Reset menu when navigating
function NavigationMenu() {
  const router = useRouter();
  const menuOpen = useToggleWithEffect(false, router.pathname !== router.asPath);

  return (
    <>
      <button onClick={menuOpen.toggle}>Menu</button>
      {menuOpen.value && <nav>Navigation items</nav>}
    </>
  );
}

# Toggle with LocalStorage

Persist toggle state:

typescript
export function useTogglePersisted(
  key: string,
  initialValue: boolean = false
) {
  const [value, setValue] = useState(() => {
    if (typeof window === 'undefined') return initialValue;

    const stored = localStorage.getItem(key);
    return stored !== null ? JSON.parse(stored) : initialValue;
  });

  const toggle = useCallback(() => {
    setValue(prev => {
      const newValue = !prev;
      localStorage.setItem(key, JSON.stringify(newValue));
      return newValue;
    });
  }, [key]);

  const on = useCallback(() => {
    setValue(true);
    localStorage.setItem(key, 'true');
  }, [key]);

  const off = useCallback(() => {
    setValue(false);
    localStorage.setItem(key, 'false');
  }, [key]);

  return { value, toggle, setValue, on, off };
}

// Usage: Dark mode preference persists across sessions
function App() {
  const darkMode = useTogglePersisted('darkMode', false);

  return (
    <div style={{ background: darkMode.value ? '#000' : '#fff' }}>
      <button onClick={darkMode.toggle}>
        {darkMode.value ? 'Light' : 'Dark'} mode
      </button>
    </div>
  );
}

# Practical Application Scenarios

# Scenario 1: Modal/Dialog Toggle

typescript
export function ModalDialog({
  title,
  children,
}: {
  title: string;
  children: ReactNode;
}) {
  const modal = useToggle(false);

  return (
    <>
      <button onClick={modal.on}>Open {title}</button>

      {modal.value && (
        <div className="modal-backdrop" onClick={modal.off}>
          <div className="modal" onClick={e => e.stopPropagation()}>
            <h2>{title}</h2>
            {children}
            <button onClick={modal.off}>Close</button>
          </div>
        </div>
      )}
    </>
  );
}

// Usage
function App() {
  return (
    <>
      <h1>My App</h1>
      <ModalDialog title="Settings">
        <p>Configure your preferences here.</p>
      </ModalDialog>
    </>
  );
}

# Scenario 2: Form Field Visibility

typescript
export function PasswordInput() {
  const showPassword = useToggle(false);

  return (
    <div className="password-field">
      <input
        type={showPassword.value ? 'text' : 'password'}
        placeholder="Enter password"
      />
      <button onClick={showPassword.toggle}>
        {showPassword.value ? 'Hide' : 'Show'}
      </button>
    </div>
  );
}

# Scenario 3: Accordion/Collapsible Section

typescript
export function AccordionItem({
  title,
  children,
}: {
  title: string;
  children: ReactNode;
}) {
  const expanded = useToggle(false);

  return (
    <div className="accordion-item">
      <button
        className="accordion-header"
        onClick={expanded.toggle}
        aria-expanded={expanded.value}
      >
        <span>{title}</span>
        <span className={`icon ${expanded.value ? 'open' : ''}`}>▼</span>
      </button>

      {expanded.value && (
        <div className="accordion-content">{children}</div>
      )}
    </div>
  );
}

// Usage
function FAQ() {
  return (
    <>
      <AccordionItem title="What is this?">
        <p>This is an accordion component.</p>
      </AccordionItem>

      <AccordionItem title="How does it work?">
        <p>Click the header to expand or collapse.</p>
      </AccordionItem>
    </>
  );
}

# Scenario 4: Feature Flags and Experimental UI

typescript
export function FeatureFlag({
  flag,
  experimental,
  production,
}: {
  flag: boolean;
  experimental: ReactNode;
  production: ReactNode;
}) {
  return flag ? experimental : production;
}

// In component
export function Dashboard() {
  const betaUI = useToggle(false);

  return (
    <div>
      <button onClick={betaUI.toggle}>
        {betaUI.value ? 'Disable' : 'Enable'} Beta UI
      </button>

      <FeatureFlag
        flag={betaUI.value}
        production={<OldDashboard />}
        experimental={<NewBetaDashboard />}
      />
    </div>
  );
}

# Scenario 5: Loading State Toggle

typescript
export function useAsyncToggle<T>(
  asyncFn: () => Promise<T>
) {
  const loading = useToggle(false);
  const [data, setData] = useState<T | null>(null);
  const [error, setError] = useState<Error | null>(null);

  const execute = useCallback(async () => {
    loading.on();
    setError(null);

    try {
      const result = await asyncFn();
      setData(result);
    } catch (err) {
      setError(err instanceof Error ? err : new Error('Unknown error'));
    } finally {
      loading.off();
    }
  }, [asyncFn, loading]);

  return {
    isLoading: loading.value,
    data,
    error,
    execute,
  };
}

// Usage
function DataFetcher() {
  const { isLoading, data, error, execute } = useAsyncToggle(
    () => fetch('/api/data').then(r => r.json())
  );

  return (
    <>
      <button onClick={execute} disabled={isLoading}>
        {isLoading ? 'Loading...' : 'Fetch Data'}
      </button>

      {data && <pre>{JSON.stringify(data, null, 2)}</pre>}
      {error && <p style={{ color: 'red' }}>Error: {error.message}</p>}
    </>
  );
}

# Scenario 6: Theme Toggle with Effects

typescript
export function ThemeToggle() {
  const darkMode = useToggleAdvanced({
    initialValue: false,
    onOn: () => {
      document.documentElement.classList.add('dark');
      document.documentElement.style.colorScheme = 'dark';
    },
    onOff: () => {
      document.documentElement.classList.remove('dark');
      document.documentElement.style.colorScheme = 'light';
    },
  });

  return (
    <button onClick={darkMode.toggle}>
      {darkMode.value ? '☀️ Light' : '🌙 Dark'}
    </button>
  );
}

# Performance Considerations

# useCallback for Stable Functions

The hook uses useCallback to ensure toggle functions maintain stable references:

typescript
// ✅ Stable reference across renders
const { toggle } = useToggle(false);

// If toggle wasn't memoized, this would cause unnecessary re-renders:
<ChildComponent onToggle={toggle} />

# Avoiding Unnecessary Re-renders

Since toggle state is simple boolean, updates are inherently efficient. But when composing with other state:

typescript
export function useToggleWithDependents(
  initialValue: boolean = false,
  dependentState?: any
) {
  const toggle = useToggle(initialValue);

  // Only re-compute when toggle or dependent state changes
  const computedValue = useMemo(
    () => someExpensiveComputation(toggle.value, dependentState),
    [toggle.value, dependentState]
  );

  return { ...toggle, computedValue };
}

# FAQ

# Q: Why not just use useState(!value) inline?

A: useToggle clarifies intent. When someone sees onClick={toggle}, it's immediately clear what happens. With onClick={() => setValue(!value)}, you need to read the implementation. useToggle is about code clarity and reusability.

# Q: Should I memoize the toggle functions?

A: Yes, using useCallback is recommended when passing toggle functions to child components to prevent unnecessary re-renders. The basic implementation includes this.

# Q: Can I use this with TypeScript?

A: Yes, the hook is fully typed. The interface clearly defines the return object shape.

# Q: What if I need to toggle based on a condition?

A: Use the setValue method:

typescript
const isOpen = useToggle(false);

// Toggle based on condition
if (someCondition) {
  isOpen.setValue(true);
} else {
  isOpen.setValue(false);
}

# Q: How do I test a component using useToggle?

A: Just test the behavior:

typescript
test('toggle button works', () => {
  const { getByRole } = render(<ToggleButton />);
  const button = getByRole('button');

  expect(button).toHaveTextContent('Show');

  act(() => {
    button.click();
  });

  expect(button).toHaveTextContent('Hide');
});

# Q: Can I combine multiple toggles?

A: Yes, just use multiple hooks:

typescript
function ComplexUI() {
  const menu = useToggle(false);
  const sidebar = useToggle(true);
  const notifications = useToggle(false);

  // Use all three independently
  return (
    <div>
      <button onClick={menu.toggle}>Menu</button>
      <button onClick={sidebar.toggle}>Sidebar</button>
      <button onClick={notifications.toggle}>Notifications</button>
    </div>
  );
}

# Q: Is useToggle in React by default?

A: No, you need to implement it or use a library. This hook is simple enough to copy into every project or publish as a reusable utility.

# Common Patterns

Pattern 1: Controlled Toggle

typescript
export function ControlledToggle({
  value,
  onChange,
}: {
  value: boolean;
  onChange: (value: boolean) => void;
}) {
  const toggle = useToggleAdvanced({
    initialValue: value,
    onChange,
  });

  return (
    <button onClick={toggle.toggle}>
      {toggle.value ? 'On' : 'Off'}
    </button>
  );
}

Pattern 2: Toggle with Reset

typescript
export function useToggleWithReset(
  initialValue: boolean = false
) {
  const toggle = useToggle(initialValue);

  const reset = useCallback(() => {
    toggle.setValue(initialValue);
  }, [initialValue, toggle]);

  return { ...toggle, reset };
}

Pattern 3: Timeout Toggle (Auto-reset)

typescript
export function useToggleWithTimeout(
  initialValue: boolean = false,
  timeoutMs: number = 5000
) {
  const toggle = useToggle(initialValue);
  const timeoutIdRef = useRef<NodeJS.Timeout | null>(null);

  const toggleWithTimeout = useCallback(() => {
    toggle.toggle();

    // Clear previous timeout
    if (timeoutIdRef.current) {
      clearTimeout(timeoutIdRef.current);
    }

    // Auto-reset after timeout
    timeoutIdRef.current = setTimeout(() => {
      toggle.setValue(initialValue);
    }, timeoutMs);
  }, [toggle, initialValue, timeoutMs]);

  useEffect(() => {
    return () => {
      if (timeoutIdRef.current) {
        clearTimeout(timeoutIdRef.current);
      }
    };
  }, []);

  return { ...toggle, toggleWithTimeout };
}


# Next Steps

The useToggle hook is deceptively simple but foundational. It teaches you:

  • How to abstract repetitive state patterns
  • Why useCallback matters for performance
  • How to compose hooks for richer behavior
  • The power of intention-revealing code

From here, explore:

  • Combining with useReducer for complex toggle logic
  • Building a useAsync hook for loading states
  • Creating a custom form hook using multiple toggles
  • Implementing a state machine for multi-step toggles

Start with the basic implementation in your projects. As your application scales, you'll appreciate the clarity and reusability this hook provides. Whether building a design system or a simple feature, useToggle is the foundation of clean boolean state management.

What patterns do you use toggles for most? Share your implementations in the comments—theme switching, modal management, and feature flags always spark interesting discussions about managing UI state elegantly.

Sponsored Content

Google AdSense Placeholder

CONTENT SLOT

Sponsored

Google AdSense Placeholder

FOOTER SLOT