import type {BaseSyntheticEvent} from 'react'; import {useCallback, useState} from 'react'; import {useSyncedRef} from '../useSyncedRef/index.js'; import type {InitialState, NextState} from '../util/resolve-hook-state.js'; import {resolveHookState} from '../util/resolve-hook-state.js'; export function useToggle( initialState: InitialState, ignoreReactEvents: false, ): [boolean, (nextState?: NextState) => void]; export function useToggle( initialState?: InitialState, ignoreReactEvents?: true, ): [boolean, (nextState?: NextState | BaseSyntheticEvent) => void]; /** * Like `useState`, but can only become `true` or `false`. * * State setter, in case called without arguments, will change the state to opposite. React * synthetic events are ignored by default so state setter can be used as event handler directly, * such behaviour can be changed by setting 2nd parameter to `false`. */ export function useToggle( initialState: InitialState = false, ignoreReactEvents = true, ): [boolean, (nextState?: NextState | BaseSyntheticEvent) => void] { // We don't use useReducer (which would end up with less code), because exposed // action does not provide functional updates feature. // Therefore, we have to create and expose our own state setter with // toggle logic. const [state, setState] = useState(initialState); const ignoreReactEventsRef = useSyncedRef(ignoreReactEvents); return [ state, useCallback((nextState) => { setState((previousState) => { if ( nextState === undefined || (ignoreReactEventsRef.current && typeof nextState === 'object' && (nextState.constructor.name === 'SyntheticBaseEvent' || // @ts-expect-error React internals typeof nextState._reactName === 'string')) ) { return !previousState; } return Boolean(resolveHookState(nextState, previousState)); }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []), ]; }