diff --git a/docs/useSwitcher.md b/docs/useSwitcher.md new file mode 100644 index 0000000000..850da542b8 --- /dev/null +++ b/docs/useSwitcher.md @@ -0,0 +1,101 @@ +# `useSwitcher` + +React hook that tracks a boolean state with dedicated functions for turning on, off, and toggling. + +Similar to `useToggle`, but instead of a single toggle function, it provides three separate control functions for more explicit state management. + +## Usage + +```jsx +import { useSwitcher } from 'react-use'; + +const Demo = () => { + const [isOn, turnOn, turnOff, toggle] = useSwitcher(); + + return ( +
+
State: {isOn ? 'ON' : 'OFF'}
+ + + +
+ ); +}; +``` + +## Examples + +### With initial value + +```jsx +const [isOpen, openModal, closeModal, toggleModal] = useSwitcher(true); +``` + +### In a modal component + +```jsx +const Modal = () => { + const [isOpen, openModal, closeModal] = useSwitcher(false); + + return ( + <> + + {isOpen && ( +
+

Modal Content

+ +
+ )} + + ); +}; +``` + +### In a sidebar component + +```jsx +const Sidebar = () => { + const [isVisible, showSidebar, hideSidebar, toggleSidebar] = useSwitcher(true); + + return ( + <> + + + + ); +}; +``` + +## Reference + +```typescript +const [state, turnOn, turnOff, toggle] = useSwitcher(defaultValue?); +``` + +### Parameters + +- `defaultValue`: `boolean` - Initial state value. Defaults to `false`. + +### Returns + +Returns a tuple with the following elements: + +- `state`: `boolean` - Current state value. +- `turnOn`: `() => void` - Function that sets state to `true`. +- `turnOff`: `() => void` - Function that sets state to `false`. +- `toggle`: `() => void` - Function that toggles the state. + +## Related hooks + +- [`useToggle`](./useToggle.md) - Similar hook with a single toggle function +- [`useBoolean`](./useBoolean.md) - Alias for `useToggle` diff --git a/src/index.ts b/src/index.ts index 62b69356b7..6be5c1a5d1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -89,6 +89,7 @@ export { default as useSpeech } from './useSpeech'; export { default as useStartTyping } from './useStartTyping'; export { useStateWithHistory } from './useStateWithHistory'; export { default as useStateList } from './useStateList'; +export { default as useSwitcher } from './useSwitcher'; export { default as useThrottle } from './useThrottle'; export { default as useThrottleFn } from './useThrottleFn'; export { default as useTimeout } from './useTimeout'; diff --git a/src/useSwitcher.ts b/src/useSwitcher.ts new file mode 100644 index 0000000000..9cade6699e --- /dev/null +++ b/src/useSwitcher.ts @@ -0,0 +1,18 @@ +import { useCallback, useState } from 'react'; + +/** + * @param defaultValue initial value of the switch. Default {@link false} + * @example + * const [isOpen, turnIsOpenOn, turnIsOpenOff, toggleIsOpen] = useSwitcher(); + */ +const useSwitcher = (defaultValue: boolean = false): readonly [boolean, () => void, () => void, () => void] => { + const [state, setState] = useState(defaultValue); + + const turnOn = useCallback(() => setState(true), []); + const turnOff = useCallback(() => setState(false), []); + const toggle = useCallback(() => setState((s) => !s), []); + + return [state, turnOn, turnOff, toggle] as const; +}; + +export default useSwitcher; diff --git a/tests/useSwitcher.test.ts b/tests/useSwitcher.test.ts new file mode 100644 index 0000000000..44e225724d --- /dev/null +++ b/tests/useSwitcher.test.ts @@ -0,0 +1,164 @@ +import { act, renderHook } from '@testing-library/react-hooks'; +import useSwitcher from '../src/useSwitcher'; + +const setUp = (initialValue?: boolean) => renderHook(() => useSwitcher(initialValue)); + +describe('useSwitcher', () => { + it('should init state to false by default', () => { + const { result } = setUp(); + + expect(result.current[0]).toBe(false); + expect(typeof result.current[1]).toBe('function'); + expect(typeof result.current[2]).toBe('function'); + expect(typeof result.current[3]).toBe('function'); + }); + + it('should init state to false when explicitly passed', () => { + const { result } = setUp(false); + + expect(result.current[0]).toBe(false); + }); + + it('should init state to true when passed', () => { + const { result } = setUp(true); + + expect(result.current[0]).toBe(true); + }); + + it('should turn on from false', () => { + const { result } = setUp(false); + const [, turnOn] = result.current; + + expect(result.current[0]).toBe(false); + + act(() => { + turnOn(); + }); + + expect(result.current[0]).toBe(true); + }); + + it('should stay on when turn on is called while already on', () => { + const { result } = setUp(true); + const [, turnOn] = result.current; + + expect(result.current[0]).toBe(true); + + act(() => { + turnOn(); + }); + + expect(result.current[0]).toBe(true); + }); + + it('should turn off from true', () => { + const { result } = setUp(true); + const [, , turnOff] = result.current; + + expect(result.current[0]).toBe(true); + + act(() => { + turnOff(); + }); + + expect(result.current[0]).toBe(false); + }); + + it('should stay off when turn off is called while already off', () => { + const { result } = setUp(false); + const [, , turnOff] = result.current; + + expect(result.current[0]).toBe(false); + + act(() => { + turnOff(); + }); + + expect(result.current[0]).toBe(false); + }); + + it('should toggle state from true to false', () => { + const { result } = setUp(true); + const [, , , toggle] = result.current; + + act(() => { + toggle(); + }); + + expect(result.current[0]).toBe(false); + }); + + it('should toggle state from false to true', () => { + const { result } = setUp(false); + const [, , , toggle] = result.current; + + act(() => { + toggle(); + }); + + expect(result.current[0]).toBe(true); + }); + + it('should toggle multiple times correctly', () => { + const { result } = setUp(false); + const [, , , toggle] = result.current; + + expect(result.current[0]).toBe(false); + + act(() => { + toggle(); + }); + expect(result.current[0]).toBe(true); + + act(() => { + toggle(); + }); + expect(result.current[0]).toBe(false); + + act(() => { + toggle(); + }); + expect(result.current[0]).toBe(true); + }); + + it('should work with all functions in combination', () => { + const { result } = setUp(false); + const [, turnOn, turnOff, toggle] = result.current; + + expect(result.current[0]).toBe(false); + + act(() => { + turnOn(); + }); + expect(result.current[0]).toBe(true); + + act(() => { + toggle(); + }); + expect(result.current[0]).toBe(false); + + act(() => { + turnOn(); + }); + expect(result.current[0]).toBe(true); + + act(() => { + turnOff(); + }); + expect(result.current[0]).toBe(false); + }); + + it('should maintain function references across re-renders', () => { + const { result, rerender } = setUp(false); + + const [, turnOn1, turnOff1, toggle1] = result.current; + + rerender(); + + const [, turnOn2, turnOff2, toggle2] = result.current; + + expect(turnOn1).toBe(turnOn2); + expect(turnOff1).toBe(turnOff2); + expect(toggle1).toBe(toggle2); + }); +});