Hooks

useToggle

A boolean-state hook with a fixed toggle/setTrue/setFalse/set action set that keeps a stable reference across renders.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"

export interface UseToggleActions {
  /** Flips the current value. Functional update — safe to call from stale closures. */
  toggle: () => void
  /** Sets the value to `true` (e.g. open a drawer/panel). */
  setTrue: () => void
  /** Sets the value to `false` (e.g. close a drawer/panel). */
  setFalse: () => void
  /** Sets the value to an explicit boolean. */
  set: (next: boolean) => void
}

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/use-toggle.json

Prompt

Build a React + TypeScript "useToggle" hook (no dependencies beyond React).

Contract
- `useToggle(initial?: boolean): [value: boolean, actions: { toggle: () => void;
  setTrue: () => void; setFalse: () => void; set: (next: boolean) => void }]`.
- `initial` defaults to `false`, captured once on mount — same semantics as
  `useState`'s initial argument (a different literal on a later render doesn't
  reset an already-initialized value).
- Returns a 2-tuple `[value, actions]`, not a flat object — mirrors `useState`'s
  call shape so it reads like a drop-in boolean `useState` with a few extra
  named actions bolted on.

Behavior
- `toggle()` flips `value` via a functional update (`v => !v`), so it never
  reads a `value` captured in a stale closure — two toggles queued in the same
  tick (e.g. two batched event handlers) both apply correctly instead of one
  silently clobbering the other's read.
- `setTrue()` / `setFalse()` jump straight to a known value regardless of the
  current one — safe to wire to two independent triggers (an "open" button and
  a "close" button) without them ever racing into an inconsistent state the
  way two blind `toggle()` calls could.
- `set(next)` sets an arbitrary boolean directly, for callers that already
  computed the target value.
- Every action is wrapped in `useCallback` with an empty dependency array
  (the underlying `useState` setter is already stable, so none of the actions
  need to close over `value`), then grouped into one `actions` object with
  `useMemo`. Both the individual actions and the `actions` object itself keep
  the same reference across every re-render of the calling component.

Rendering & styling
- The hook renders nothing and touches no DOM. Consumers own all UI — style
  open/closed states with semantic tokens (`bg-primary`, `bg-muted`,
  `bg-card`, `text-muted-foreground`, `border`) and respect
  `prefers-reduced-motion` for any transition tied to the toggle.

Customization levers
- Rename the hook/return shape to a `useDisclosure`-style API
  (`{ isOpen, onOpen, onClose, onToggle }`) if that naming convention is
  preferred elsewhere in the codebase — the implementation (functional
  update + stable callbacks) doesn't change, only the labels.
- Controlled mode — accept optional `value`/`onChange` props and, when
  present, call `onChange` instead of the internal setter (mirroring a
  controlled/uncontrolled native input) if a parent needs to own the boolean;
  deliberately left out by default to keep the hook a plain drop-in
  `useState` replacement.

Concepts

  • Functional update, not read-then-writetoggle() calls setValue(v => !v), so it never depends on the value captured in whatever closure it was defined in; two toggles queued in the same tick always both apply instead of one overwriting the other's stale read.
  • Fixed action set beats an ad-hoc setter — instead of exposing a raw setValue, the hook commits to toggle/setTrue/setFalse/set, so call sites read as intent ("open the drawer") rather than a boolean flip a reader has to reverse-engineer.
  • Stable callback identity — every action (and the actions object) is memoized with an empty dependency array, so its reference never changes between renders — unlike an inline const toggle = () => setValue(!value), which is a new function every render and would force any effect/useCallback/memoized child depending on it to re-run or re-render needlessly.
  • setTrue/setFalse are idempotent, toggle isn't — calling setFalse() twice in a row is a no-op the second time; calling toggle() twice flips back to where you started. Prefer the named actions whenever two independent triggers should never race into an inconsistent state.
  • Multi-instance independence — the hook holds no module-level state, so a list of accordion panels or drawers each call useToggle() on their own and never step on each other's value.

On This Page