React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

togglebooleanstatesidebardark-modecustom-hook

useToggle — Simple boolean toggle with on, off, and toggle controls

Recipe

import { useState, useCallback, useMemo } from "react";
 
interface UseToggleReturn {
  /** Current boolean value */
  value: boolean;
  /** Flip the value */
  toggle: () => void;
  /** Set to true */
  on: () => void;
  /** Set to false */
  off: () => void;
  /** Set to a specific value */
  set: (value: boolean) => void;
}
 
function useToggle(initialValue: boolean = false): UseToggleReturn {
  const [value, setValue] = useState(initialValue);
 
  const toggle = useCallback(() => setValue((v) => !v), []);
  const on = useCallback(() => setValue(true), []);
  const off = useCallback(() => setValue(false), []);
 
  return useMemo(
    () => ({ value, toggle, on, off, set: setValue }),
    [value, toggle, on, off]
  );
}

When to reach for this: You have a boolean state (modal open, sidebar visible, dark mode, feature flag) and want a cleaner API than raw useState with !prev toggling scattered through your JSX.

Working Example

"use client";
 
function Sidebar() {
  const sidebar = useToggle(false);
 
  return (
    <div style={{ display: "flex" }}>
      <aside
        style={{
          width: sidebar.value ? 250 : 0,
          overflow: "hidden",
          transition: "width 0.3s",
          background: "#f5f5f5",
          borderRight: "1px solid #e0e0e0",
        }}
      >
        <nav style={{ padding: 16, whiteSpace: "nowrap" }}>
          <ul style={{ listStyle: "none", padding: 0 }}>
            <li>Dashboard</li>
            <li>Settings</li>
            <li>Profile</li>
          </ul>
        </nav>
      </aside>
      <main style={{ flex: 1, padding: 16 }}>
        <button onClick={sidebar.toggle}>
          {sidebar.value ? "Close" : "Open"} Sidebar
        </button>
        <p>Main content here</p>
      </main>
    </div>
  );
}
 
function DarkModeToggle() {
  const darkMode = useToggle(false);
 
  return (
    <div
      style={{
        background: darkMode.value ? "#1a1a2e" : "#ffffff",
        color: darkMode.value ? "#e0e0e0" : "#333",
        padding: 24,
        transition: "all 0.3s",
      }}
    >
      <label style={{ display: "flex", alignItems: "center", gap: 8 }}>
        <input
          type="checkbox"
          checked={darkMode.value}
          onChange={darkMode.toggle}
        />
        Dark Mode
      </label>
    </div>
  );
}
 
function FeatureFlags() {
  const betaFeature = useToggle(false);
  const notifications = useToggle(true);
 
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
      <label>
        <input
          type="checkbox"
          checked={betaFeature.value}
          onChange={betaFeature.toggle}
        />
        Beta Features
      </label>
      <label>
        <input
          type="checkbox"
          checked={notifications.value}
          onChange={notifications.toggle}
        />
        Notifications
      </label>
      <div style={{ display: "flex", gap: 8 }}>
        <button onClick={notifications.on}>Enable All</button>
        <button onClick={notifications.off}>Disable All</button>
      </div>
    </div>
  );
}

What this demonstrates:

  • sidebar.toggle replaces () => setOpen(!open) with a cleaner API
  • darkMode.value reads naturally in template expressions
  • notifications.on and notifications.off provide explicit set-to-true/false calls
  • Multiple toggles are independent and can be composed freely

Deep Dive

How It Works

  • toggle uses a functional update (v) => !v to always flip based on the current value, avoiding stale closure issues.
  • on and off set absolute values, useful for reset buttons or event handlers that should always set a specific state.
  • useCallback memoizes all functions so they are referentially stable across renders, safe to pass as props or use in dependency arrays.
  • useMemo wraps the return object so consumers get a stable reference when only the functions (which are already stable) are used.

Parameters & Return Values

ParameterTypeDefaultDescription
initialValuebooleanfalseStarting value
ReturnTypeDescription
valuebooleanCurrent state
toggle() => voidFlip the value
on() => voidSet to true
off() => voidSet to false
set(value: boolean) => voidSet to any boolean

Variations

Tuple return: For a more useState-like API, return a tuple:

function useToggle(initial = false): [boolean, () => void] {
  const [value, setValue] = useState(initial);
  const toggle = useCallback(() => setValue((v) => !v), []);
  return [value, toggle];
}
 
// Usage
const [isOpen, toggleOpen] = useToggle();

With reducer: For complex toggle logic with action history:

function useToggle(initial = false) {
  const [value, dispatch] = useReducer(
    (state: boolean, action: "toggle" | "on" | "off") => {
      switch (action) {
        case "toggle": return !state;
        case "on": return true;
        case "off": return false;
      }
    },
    initial
  );
 
  return { value, dispatch };
}

Persistent toggle: Combine with useLocalStorage for toggles that survive refresh:

function usePersistentToggle(key: string, initial = false) {
  const [value, setValue] = useLocalStorage(key, initial);
  const toggle = useCallback(() => setValue((v) => !v), [setValue]);
  const on = useCallback(() => setValue(true), [setValue]);
  const off = useCallback(() => setValue(false), [setValue]);
  return { value, toggle, on, off };
}

TypeScript Notes

  • The return type is a named interface, making it easy to pass the toggle object as a prop.
  • No generics needed since the value is always boolean.
  • The set function accepts boolean directly (not a union with updater function) to keep the API simple.

Gotchas

  • Over-engineering — For a single toggle used once, plain useState(false) is fine. Fix: Reach for useToggle when you have multiple toggles or want the semantic on/off/toggle API.
  • Object identity — Without useMemo, the returned object creates a new reference every render, which can cause unnecessary re-renders in children. Fix: The useMemo in the recipe handles this.
  • Controlled vs uncontrolled — This hook owns its own state. If the parent needs to control the value, pass the state down as props instead. Fix: Use useToggle for local UI state, not shared state.

Alternatives

PackageHook NameNotes
usehooks-tsuseToggleTuple return [value, toggle]
@uidotdev/usehooksuseToggleMinimal, tuple return
ahooksuseToggleSupports non-boolean toggle between two values
react-useuseToggleSimple boolean toggle

FAQs

Why use useToggle instead of plain useState(false)?
  • useToggle provides a semantic API: toggle(), on(), off(), and set().
  • It eliminates scattered !prev toggling in JSX.
  • It is most useful when you have multiple toggles or want clearer intent in event handlers.
What is the difference between toggle(), on(), off(), and set()?
  • toggle() flips the current value to its opposite.
  • on() always sets the value to true.
  • off() always sets the value to false.
  • set(value) sets the value to any boolean you pass.
Why does the hook wrap the return object in useMemo?

Without useMemo, a new object reference is created every render. This can cause unnecessary re-renders in child components that receive the toggle object as a prop. useMemo keeps the reference stable when only the functions (already memoized) are used.

Why does toggle() use a functional update (v) => !v instead of !value?

The functional update reads the current state at the time of execution, not at the time the function was created. This avoids stale closure bugs when toggle() is called from event handlers or effects.

Can I use the tuple return pattern instead of the object pattern?

Yes. The Variations section shows a tuple variant:

const [isOpen, toggleOpen] = useToggle();

Use the tuple for simple cases and the object for multiple controls (on, off, set).

How do I make a toggle persist across page refreshes?

Combine with useLocalStorage:

function usePersistentToggle(key: string, initial = false) {
  const [value, setValue] = useLocalStorage(key, initial);
  const toggle = useCallback(() => setValue((v) => !v), [setValue]);
  return { value, toggle };
}
Gotcha: I passed the toggle object as a prop and my child re-renders every time. Why?

If you destructure and re-create the object in the parent, the reference changes. Pass the useMemo-wrapped object directly, or pass individual stable functions (toggle, on, off) as separate props.

Gotcha: I used useToggle for state that the parent also needs to control. Is that correct?

No. useToggle owns its own state (uncontrolled). If the parent needs to control the value, pass the boolean as a prop and use callbacks. Use useToggle only for local UI state.

What is the TypeScript return type of useToggle?

It returns UseToggleReturn, an interface with value: boolean, toggle: () => void, on: () => void, off: () => void, and set: (value: boolean) => void. No generics are needed.

Can I use useReducer instead of useState for the toggle?

Yes. The Variations section shows a reducer-based variant that dispatches string actions like "toggle", "on", and "off". This is useful for complex toggle logic or action history tracking.