From df3954ea5f1a34d3a058e8be5ccabcc9cc9cd981 Mon Sep 17 00:00:00 2001 From: manoahLinks Date: Sun, 30 Aug 2026 17:41:43 +0100 Subject: [PATCH] feat(hooks): add use-boolean, use-previous and use-click-outside hooks Closes #291 Closes #290 Closes #289 --- src/lib/hooks/use-boolean.ts | 21 +++++++++++++++++++ src/lib/hooks/use-click-outside.ts | 33 ++++++++++++++++++++++++++++++ src/lib/hooks/use-previous.ts | 19 +++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 src/lib/hooks/use-boolean.ts create mode 100644 src/lib/hooks/use-click-outside.ts create mode 100644 src/lib/hooks/use-previous.ts diff --git a/src/lib/hooks/use-boolean.ts b/src/lib/hooks/use-boolean.ts new file mode 100644 index 0000000..6a6f4bc --- /dev/null +++ b/src/lib/hooks/use-boolean.ts @@ -0,0 +1,21 @@ +"use client"; + +import { useCallback, useState } from "react"; + +/** + * Reusable boolean toggle hook for open/close, active/inactive states (#291). + * + * Returns a tuple of the boolean value and three memoized control functions: + * `setTrue`, `setFalse`, and `toggle`. + */ +export function useBoolean( + initialValue = false, +): readonly [boolean, () => void, () => void, () => void] { + const [value, setValue] = useState(initialValue); + + const setTrue = useCallback(() => setValue(true), []); + const setFalse = useCallback(() => setValue(false), []); + const toggle = useCallback(() => setValue((prev) => !prev), []); + + return [value, setTrue, setFalse, toggle]; +} \ No newline at end of file diff --git a/src/lib/hooks/use-click-outside.ts b/src/lib/hooks/use-click-outside.ts new file mode 100644 index 0000000..4dbe119 --- /dev/null +++ b/src/lib/hooks/use-click-outside.ts @@ -0,0 +1,33 @@ +"use client"; + +import { useEffect } from "react"; +import type { RefObject } from "react"; + +type ClickOutsideRef = RefObject; + +/** + * Trigger a callback when a click occurs outside the given ref(s) (#289). + * + * Accepts a single ref or an array of refs (e.g. a dropdown trigger and its + * menu), listens on `mousedown`, and removes the listener on unmount. + */ +export function useClickOutside( + refs: ClickOutsideRef | ClickOutsideRef[], + handler: (event: MouseEvent) => void, +) { + useEffect(() => { + const clickOutsideRefs = Array.isArray(refs) ? refs : [refs]; + + const onMouseDown = (event: MouseEvent) => { + const target = event.target as Node; + const isOutside = clickOutsideRefs.every( + (ref) => !ref.current || !ref.current.contains(target), + ); + + if (isOutside) handler(event); + }; + + document.addEventListener("mousedown", onMouseDown); + return () => document.removeEventListener("mousedown", onMouseDown); + }, [refs, handler]); +} \ No newline at end of file diff --git a/src/lib/hooks/use-previous.ts b/src/lib/hooks/use-previous.ts new file mode 100644 index 0000000..d4ed4a0 --- /dev/null +++ b/src/lib/hooks/use-previous.ts @@ -0,0 +1,19 @@ +"use client"; + +import { useEffect, useRef } from "react"; + +/** + * Access the previous value of a state or prop (#290). + * + * Returns `undefined` on the first render and the value from the previous + * render on subsequent renders, updating after the component re-renders. + */ +export function usePrevious(value: T): T | undefined { + const previousRef = useRef(undefined); + + useEffect(() => { + previousRef.current = value; + }, [value]); + + return previousRef.current; +} \ No newline at end of file