useToggle
EditToggles between two values while preserving their exact union type.
import { useCallback, useEffect, useLayoutEffect, useState,} from "react";
type Callable = (...args: never[]) => unknown;
type NonCallable<Value> = Value extends Callable ? never : Value;
export type ToggleStateAction<Value> = | NonCallable<Value> | ((this: void, value: Value) => Value);
export interface UseToggleResult<First, Second> { readonly value: First | Second; readonly setValue: ( action: ToggleStateAction<First | Second>, ) => void; readonly toggle: () => void;}
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
export function useToggle<const First, const Second>( first: First, second: Second,): UseToggleResult<First, Second>;export function useToggle<const First, const Second>( first: First, second: Second, initialValue: First | Second,): UseToggleResult<First, Second>;export function useToggle<First, Second>( first: First, second: Second, initialValue?: First | Second,): UseToggleResult<First, Second> { const initial = arguments.length >= 3 ? initialValue : first;
if (!Object.is(initial, first) && !Object.is(initial, second)) { throw new RangeError("initialValue must be one of the two toggle values"); }
const [value, setInternalValue] = useState<First | Second>( () => initial as First | Second, ); const valueIsConfigured = Object.is(value, first) || Object.is(value, second); const currentValue = valueIsConfigured ? value : first;
useIsomorphicLayoutEffect(() => { if (!valueIsConfigured) setInternalValue(() => first); }, [first, valueIsConfigured]);
const setValue = useCallback( (action: ToggleStateAction<First | Second>) => { setInternalValue((current) => { const configuredCurrent = Object.is(current, first) || Object.is(current, second) ? current : first; const next = typeof action === "function" ? (action as ( this: void, value: First | Second, ) => First | Second)( configuredCurrent, ) : action;
if (!Object.is(next, first) && !Object.is(next, second)) { throw new RangeError("value must be one of the two toggle values"); }
return next; }); }, [first, second], );
const toggle = useCallback(() => { setInternalValue((current) => { const configuredCurrent = Object.is(current, first) || Object.is(current, second) ? current : first; return Object.is(configuredCurrent, first) ? second : first; }); }, [first, second]);
return { value: currentValue, setValue, toggle };}Download
Section titled “Download”wget -O src/hooks/useToggle.ts https://raw.githubusercontent.com/jrTilak/lazykit/HEAD/registry/react-hooks/useToggle.tstemp_dir="$(mktemp -d)" && bunx degit jrTilak/lazykit/registry/react-hooks "$temp_dir" && mv "$temp_dir/useToggle.ts" src/hooks/useToggle.ts && rm -r "$temp_dir"Examples
Section titled “Examples”import { useToggle } from "./useToggle";
export function ToggleExample() { const { value: theme, toggle } = useToggle("light", "dark");
return ( <div> <p>Theme: {theme}</p> <button type="button" onClick={toggle}>Switch theme</button> </div> );}first(First) — First toggle value and the default initial value.second(Second) — Second toggle value.initialValue(First | Second, optional) — Which configured value to use initially. Object values are compared by identity.
Returns
Section titled “Returns”UseToggleResult<First, Second> containing value, setValue, and toggle.
Runtime validation prevents JavaScript callers and updater callbacks from
introducing a third value. If either configured value changes and the current
value no longer belongs to the pair, the hook resets to the new first value.
Callable choices are replaced through an updater such as
setValue(() => nextFunction), avoiding function-as-updater ambiguity.