UNPKG

hookify-react

Version:

A collection of optimized and reusable React hooks for state management, dom interaction, responsive design, storage, location, asynchronous management and performance improvements.

829 lines (802 loc) 28.1 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { useAdvancedEffect: () => useAdvancedEffect, useArray: () => useArray, useClickOutside: () => useClickOutside, useCopyToClipboard: () => useCopyToClipboard, useCounter: () => useCounter, useDebounce: () => useDebounce, useEventListener: () => useEventListener, useFormState: () => useFormState, useGeoLocation: () => useGeoLocation, useHistory: () => useHistory, useHover: () => useHover, useInterval: () => useInterval, useLocalStorage: () => useLocalStorage, useOnScreen: () => useOnScreen, useOnlineStatus: () => useOnlineStatus, usePress: () => usePress, usePrevious: () => usePrevious, useScrollInfo: () => useScrollInfo, useSessionStorage: () => useSessionStorage, useSize: () => useSize, useStorage: () => useStorage, useTimeout: () => useTimeout, useToggle: () => useToggle, useUpdatedEffect: () => useUpdatedEffect, useWindowSize: () => useWindowSize }); module.exports = __toCommonJS(index_exports); // src/hooks/default/useAdvancedEffect.ts var import_react = require("react"); function useAdvancedEffect(effect, deps) { const isFirstRender = (0, import_react.useRef)(true); const previousDepsRef = (0, import_react.useRef)(deps); (0, import_react.useEffect)(() => { if (isFirstRender.current) { isFirstRender.current = false; previousDepsRef.current = deps; return; } const prevDeps = previousDepsRef.current; const hasDepsChanged = prevDeps.length !== deps.length || deps.some((dep, i) => !Object.is(dep, prevDeps[i])); if (hasDepsChanged) { previousDepsRef.current = deps; return effect(); } }, deps); } // src/hooks/default/useUpdatedEffect.ts var import_react2 = require("react"); function useUpdatedEffect(effect, deps) { const isFirstRender = (0, import_react2.useRef)(true); (0, import_react2.useEffect)(() => { if (isFirstRender.current) { isFirstRender.current = false; return; } return effect(); }, deps); } // src/hooks/state-management/useArray.ts var import_react3 = require("react"); function useArray(initialValue = []) { const [state, setState] = (0, import_react3.useState)(initialValue); const stateRef = (0, import_react3.useRef)(initialValue); stateRef.current = state; const initialValueRef = (0, import_react3.useRef)(initialValue); const push = (0, import_react3.useCallback)((value) => { setState((prev) => [...prev, value]); return stateRef.current.length + 1; }, []); const pop = (0, import_react3.useCallback)(() => { const lastElement = stateRef.current[stateRef.current.length - 1]; setState((prev) => prev.slice(0, -1)); return lastElement; }, []); const unshift = (0, import_react3.useCallback)((value) => { setState((prev) => [value, ...prev]); return stateRef.current.length + 1; }, []); const shift = (0, import_react3.useCallback)(() => { const firstElement = stateRef.current[0]; setState((prev) => prev.slice(1)); return firstElement; }, []); const removeByIndex = (0, import_react3.useCallback)((index) => { setState((prev) => [...prev.slice(0, index), ...prev.slice(index + 1)]); }, []); const removeByValue = (0, import_react3.useCallback)((value) => { setState( (prev) => prev.filter( (item) => typeof value === "object" && value !== null ? JSON.stringify(item) !== JSON.stringify(value) : item !== value ) ); }, []); const clear = (0, import_react3.useCallback)(() => setState([]), []); const replace = (0, import_react3.useCallback)((newArray) => setState(newArray), []); const reset = (0, import_react3.useCallback)(() => setState(initialValueRef.current), []); const filter = (0, import_react3.useCallback)( (predicate) => { setState((prev) => prev.filter(predicate)); }, [] ); const updateByIndex = (0, import_react3.useCallback)((index, value) => { setState((prev) => prev.map((item, i) => i === index ? value : item)); }, []); const updateByValue = (0, import_react3.useCallback)((prevValue, newValue) => { setState( (prev) => prev.map((item) => { if (typeof item !== "object" || item === null) { return item === prevValue ? newValue : item; } return JSON.stringify(item) === JSON.stringify(prevValue) ? newValue : item; }) ); }, []); const methods = (0, import_react3.useRef)({ push, pop, shift, unshift, removeByIndex, removeByValue, clear, filter, reset, replace, updateByIndex, updateByValue }).current; return [state, setState, methods]; } // src/hooks/state-management/useCounter.ts var import_react4 = require("react"); function useCounter(initialValue = 0, options = {}) { const { min, max } = options; const clamp = (0, import_react4.useCallback)( (value) => { let next = value; if (typeof min === "number") next = Math.max(min, next); if (typeof max === "number") next = Math.min(max, next); return next; }, [min, max] ); const [count, setCount] = (0, import_react4.useState)(() => clamp(initialValue)); const initialValueRef = (0, import_react4.useRef)(initialValue); const increment = (0, import_react4.useCallback)( () => setCount((prev) => clamp(prev + 1)), [clamp] ); const incrementByValue = (0, import_react4.useCallback)( (value) => setCount((prev) => clamp(prev + value)), [clamp] ); const decrement = (0, import_react4.useCallback)( () => setCount((prev) => clamp(prev - 1)), [clamp] ); const decrementByValue = (0, import_react4.useCallback)( (value) => setCount((prev) => clamp(prev - value)), [clamp] ); const set = (0, import_react4.useCallback)((value) => setCount(clamp(value)), [clamp]); const reset = (0, import_react4.useCallback)( () => setCount(clamp(initialValueRef.current)), [clamp] ); return { count, increment, incrementByValue, decrement, decrementByValue, set, reset }; } // src/hooks/state-management/useFormState.ts var import_react5 = require("react"); function useFormState(defaultValue, predicates, { emptyInputValidation = true } = {}) { const resolvedDefaultValueRef = (0, import_react5.useRef)( typeof defaultValue === "function" ? defaultValue() : defaultValue ); const [state, setStateRaw] = (0, import_react5.useState)(resolvedDefaultValueRef.current); const [errors, setErrors] = (0, import_react5.useState)( () => predicates.map((predicate) => predicate(resolvedDefaultValueRef.current)) ); const stateRef = (0, import_react5.useRef)(state); stateRef.current = state; const predicatesRef = (0, import_react5.useRef)(predicates); predicatesRef.current = predicates; const emptyValidationRef = (0, import_react5.useRef)(emptyInputValidation); emptyValidationRef.current = emptyInputValidation; const setValue = (0, import_react5.useCallback)((value) => { const newValue = typeof value === "function" ? value(stateRef.current) : value; setStateRaw(newValue); if (typeof newValue === "string" && newValue.length === 0 && !emptyValidationRef.current) { setErrors([]); return; } const newErrors = predicatesRef.current.map((p) => p(newValue)); setErrors( (prevErrors) => prevErrors.length === newErrors.length && prevErrors.every((e, i) => e === newErrors[i]) ? prevErrors : newErrors ); }, []); const filteredErrors = (0, import_react5.useMemo)( () => errors.filter((error) => Boolean(error)), [errors] ); const isValid = filteredErrors.length === 0; const status = (0, import_react5.useMemo)(() => { if (resolvedDefaultValueRef.current === state && isValid) return "idle"; return isValid ? "valid" : "error"; }, [state, isValid]); return [ state, setValue, { errors: filteredErrors, isValid, status } ]; } // src/hooks/state-management/useHistory.ts var import_react6 = require("react"); function useHistory(defaultValue, { capacity = 10 } = {}) { const resolvedDefaultValue = typeof defaultValue === "function" ? defaultValue() : defaultValue; const [state, setState] = (0, import_react6.useState)(resolvedDefaultValue); const historyRef = (0, import_react6.useRef)([resolvedDefaultValue]); const pointerRef = (0, import_react6.useRef)(0); const set = (0, import_react6.useCallback)( (value) => { setState((prev) => { const resolvedValue = typeof value === "function" ? value(prev) : value; if (historyRef.current[pointerRef.current] !== resolvedValue) { if (pointerRef.current < historyRef.current.length - 1) { historyRef.current = historyRef.current.slice( 0, pointerRef.current + 1 ); } historyRef.current.push(resolvedValue); if (historyRef.current.length > capacity) { historyRef.current.shift(); } pointerRef.current = historyRef.current.length - 1; } return resolvedValue; }); }, [capacity] ); const back = (0, import_react6.useCallback)(() => { if (pointerRef.current > 0) { pointerRef.current--; setState(historyRef.current[pointerRef.current]); } }, []); const forward = (0, import_react6.useCallback)(() => { if (pointerRef.current < historyRef.current.length - 1) { pointerRef.current++; setState(historyRef.current[pointerRef.current]); } }, []); const go = (0, import_react6.useCallback)((index) => { if (index >= 0 && index < historyRef.current.length) { pointerRef.current = index; setState(historyRef.current[pointerRef.current]); } }, []); return [ state, set, { history: historyRef.current, pointer: pointerRef.current, back, forward, go } ]; } // src/hooks/state-management/usePrevious.ts var import_react7 = require("react"); function usePrevious(value) { const ref = (0, import_react7.useRef)(null); (0, import_react7.useEffect)(() => { ref.current = value; }, [value]); return ref.current; } // src/hooks/state-management/useToggle.ts var import_react8 = require("react"); function useToggle(initialValue = false) { const [state, setState] = (0, import_react8.useState)(initialValue); const toggle = (0, import_react8.useCallback)((value) => { setState((prev) => typeof value === "boolean" ? value : !prev); }, []); return [state, toggle]; } // src/hooks/async-management/useDebounce.ts var import_react10 = require("react"); // src/hooks/utils/index.ts var import_react9 = require("react"); var isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined"; var useIsomorphicLayoutEffect = isBrowser ? import_react9.useLayoutEffect : import_react9.useEffect; function useLatest(value) { const ref = (0, import_react9.useRef)(value); useIsomorphicLayoutEffect(() => { ref.current = value; }, [value]); return ref; } // src/hooks/async-management/useDebounce.ts function useDebounce(callback, delay, deps) { const callbackRef = useLatest(callback); (0, import_react10.useEffect)(() => { const timer = setTimeout(() => callbackRef.current(), delay); return () => clearTimeout(timer); }, [...deps, delay]); } // src/hooks/async-management/useTimeout.ts var import_react11 = require("react"); function useTimeout(callback, delay) { const callbackRef = useLatest(callback); const timeoutRef = (0, import_react11.useRef)(null); const clear = (0, import_react11.useCallback)(() => { if (timeoutRef.current !== null) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } }, []); const set = (0, import_react11.useCallback)(() => { clear(); timeoutRef.current = setTimeout(() => { timeoutRef.current = null; callbackRef.current(); }, delay); }, [delay, clear, callbackRef]); const reset = (0, import_react11.useCallback)(() => { set(); }, [set]); (0, import_react11.useEffect)(() => { set(); return clear; }, [set, clear]); return { set, clear, reset }; } // src/hooks/async-management/useInterval.ts var import_react12 = require("react"); function useInterval(callback, interval = 1e3) { const callbackRef = useLatest(callback); const intervalRef = (0, import_react12.useRef)(null); const clear = (0, import_react12.useCallback)(() => { if (intervalRef.current !== null) { clearInterval(intervalRef.current); intervalRef.current = null; } }, []); (0, import_react12.useEffect)(() => { if (interval === null) return; intervalRef.current = setInterval(() => callbackRef.current(), interval); return () => { if (intervalRef.current !== null) { clearInterval(intervalRef.current); intervalRef.current = null; } }; }, [interval, callbackRef]); return { clear }; } // src/hooks/storage/useStorage.ts var import_react13 = require("react"); function getStorage(type) { if (!isBrowser) return null; try { return type === "local" ? window.localStorage : window.sessionStorage; } catch { return null; } } function useStorage(key, defaultValue, type) { const readDefault = (0, import_react13.useCallback)( () => typeof defaultValue === "function" ? defaultValue() : defaultValue, [defaultValue] ); const [value, setValue] = (0, import_react13.useState)(() => { const storage = getStorage(type); if (storage) { try { const stored = storage.getItem(key); if (stored !== null) return JSON.parse(stored); } catch (error) { console.error(`useStorage: failed to read key "${key}"`, error); } } return readDefault(); }); const valueRef = (0, import_react13.useRef)(value); valueRef.current = value; (0, import_react13.useEffect)(() => { const storage = getStorage(type); if (!storage) return; try { if (value === void 0) { storage.removeItem(key); } else { storage.setItem(key, JSON.stringify(value)); } } catch (error) { console.error(`useStorage: failed to write key "${key}"`, error); } }, [key, value, type]); (0, import_react13.useEffect)(() => { if (!isBrowser || type !== "local") return; const handleStorage = (event) => { if (event.storageArea !== window.localStorage || event.key !== key) { return; } try { const next = event.newValue === null ? readDefault() : JSON.parse(event.newValue); setValue(next); } catch (error) { console.error(`useStorage: failed to sync key "${key}"`, error); } }; window.addEventListener("storage", handleStorage); return () => window.removeEventListener("storage", handleStorage); }, [key, type, readDefault]); return [value, setValue]; } function useLocalStorage(key, defaultValue) { return useStorage(key, defaultValue, "local"); } function useSessionStorage(key, defaultValue) { return useStorage(key, defaultValue, "session"); } // src/hooks/dom/useCopyToClipboard.ts var import_react14 = require("react"); function useCopyToClipboard(resetDelay = 2e3) { const [isCopied, setIsCopied] = (0, import_react14.useState)(false); const [error, setError] = (0, import_react14.useState)(null); const resetTimerRef = (0, import_react14.useRef)(null); (0, import_react14.useEffect)(() => { return () => { if (resetTimerRef.current !== null) { clearTimeout(resetTimerRef.current); } }; }, []); const copy = (0, import_react14.useCallback)( async (text) => { try { if (navigator?.clipboard?.writeText) { await navigator.clipboard.writeText(text); } else if (typeof document !== "undefined" && typeof document.execCommand === "function") { const textarea = document.createElement("textarea"); textarea.value = text; textarea.style.position = "fixed"; textarea.style.opacity = "0"; document.body.appendChild(textarea); textarea.focus(); textarea.select(); const succeeded = document.execCommand("copy"); document.body.removeChild(textarea); if (!succeeded) throw new Error("Copy command was unsuccessful"); } else { throw new Error("Clipboard API not available"); } setIsCopied(true); setError(null); if (resetDelay > 0) { if (resetTimerRef.current !== null) { clearTimeout(resetTimerRef.current); } resetTimerRef.current = setTimeout( () => setIsCopied(false), resetDelay ); } return true; } catch (err) { setError(err instanceof Error ? err : new Error(String(err))); setIsCopied(false); return false; } }, [resetDelay] ); return { copy, isCopied, error }; } // src/hooks/dom/useEventListener.ts var import_react15 = require("react"); function useEventListener(eventType, callback, elementRef, options) { const callbackRef = useLatest(callback); const optionsRef = (0, import_react15.useRef)(options); optionsRef.current = options; (0, import_react15.useEffect)(() => { const target = elementRef?.current ?? (typeof window !== "undefined" ? window : null); if (!target?.addEventListener) return; const currentOptions = optionsRef.current; const handleEvent = (event) => callbackRef.current(event); target.addEventListener(eventType, handleEvent, currentOptions); return () => { target.removeEventListener(eventType, handleEvent, currentOptions); }; }, [eventType, elementRef, callbackRef]); } // src/hooks/dom/useHover.ts var import_react16 = require("react"); function useHover() { const [isHovered, setIsHovered] = (0, import_react16.useState)(false); const ref = (0, import_react16.useRef)(null); const handleHover = (0, import_react16.useCallback)( (event) => setIsHovered(event.type === "mouseenter"), [] ); useEventListener("mouseenter", handleHover, ref); useEventListener("mouseleave", handleHover, ref); return { ref, isHovered }; } // src/hooks/dom/useOnClickOutside.ts var import_react17 = require("react"); function useClickOutside(callback) { const ref = (0, import_react17.useRef)(null); const documentRef = (0, import_react17.useRef)(isBrowser ? document : null); const handleClick = (0, import_react17.useCallback)( (event) => { const element = ref.current; if (!element || element.contains(event.target)) return; callback(event); }, [callback] ); useEventListener("mousedown", handleClick, documentRef); useEventListener("touchstart", handleClick, documentRef); return { ref }; } // src/hooks/dom/useOnlineStatus.ts var import_react18 = require("react"); function useOnlineStatus() { const [isOnline, setIsOnline] = (0, import_react18.useState)( () => isBrowser ? navigator.onLine : true ); useEventListener("online", () => setIsOnline(true)); useEventListener("offline", () => setIsOnline(false)); return { isOnline, onlineStatus: isOnline ? "online" : "offline" }; } // src/hooks/dom/useOnScreen.ts var import_react19 = require("react"); function useOnScreen(options = "0px") { const { rootMargin = "0px", threshold = 0, once = false } = typeof options === "string" ? { rootMargin: options } : options; const [isVisible, setIsVisible] = (0, import_react19.useState)(false); const ref = (0, import_react19.useRef)(null); (0, import_react19.useEffect)(() => { const target = ref.current; if (!target || typeof IntersectionObserver === "undefined") return; const observer = new IntersectionObserver( ([entry]) => { if (!entry) return; setIsVisible(entry.isIntersecting); if (entry.isIntersecting && once) observer.disconnect(); }, { rootMargin, threshold } ); observer.observe(target); return () => observer.disconnect(); }, [rootMargin, threshold, once]); return { ref, isVisible }; } // src/hooks/dom/usePress.ts var import_react20 = require("react"); function usePress() { const [isPressed, setIsPressed] = (0, import_react20.useState)(false); const ref = (0, import_react20.useRef)(null); const press = () => setIsPressed(true); const release = () => setIsPressed(false); useEventListener("mousedown", press, ref); useEventListener("touchstart", press, ref); useEventListener("mouseleave", release, ref); useEventListener("mouseup", release); useEventListener("touchend", release); return { isPressed, ref }; } // src/hooks/dom/useScrollPosition.ts var import_react21 = require("react"); function useScrollInfo() { const ref = (0, import_react21.useRef)(null); const [scrollData, setScrollData] = (0, import_react21.useState)({ scrollX: 0, scrollY: 0, scrollDirection: "none", isScrolling: false, scrollProgress: 0 }); const lastScrollY = (0, import_react21.useRef)(0); const lastScrollX = (0, import_react21.useRef)(0); const scrollTimeout = (0, import_react21.useRef)(null); const handleScroll = (0, import_react21.useCallback)(() => { const element = ref.current; const metricsSource = element ?? document.documentElement; const newScrollX = element ? element.scrollLeft : window.scrollX; const newScrollY = element ? element.scrollTop : window.scrollY; const maxScrollHeight = metricsSource.scrollHeight - metricsSource.clientHeight; const scrollPercentage = maxScrollHeight > 0 ? newScrollY / maxScrollHeight * 100 : 0; const directionX = newScrollX > lastScrollX.current ? "right" : newScrollX < lastScrollX.current ? "left" : "none"; const directionY = newScrollY > lastScrollY.current ? "down" : newScrollY < lastScrollY.current ? "up" : "none"; setScrollData({ scrollX: newScrollX, scrollY: newScrollY, scrollDirection: directionY !== "none" ? directionY : directionX, isScrolling: true, scrollProgress: scrollPercentage }); lastScrollX.current = newScrollX; lastScrollY.current = newScrollY; if (scrollTimeout.current) clearTimeout(scrollTimeout.current); scrollTimeout.current = setTimeout(() => { setScrollData((prev) => ({ ...prev, isScrolling: false })); }, 150); }, []); (0, import_react21.useEffect)(() => { const target = ref.current ?? window; target.addEventListener("scroll", handleScroll, { passive: true }); return () => { target.removeEventListener("scroll", handleScroll); if (scrollTimeout.current) { clearTimeout(scrollTimeout.current); scrollTimeout.current = null; } }; }, [handleScroll]); return { ref, ...scrollData }; } // src/hooks/dom/useSize.ts var import_react22 = require("react"); function useSize() { const ref = (0, import_react22.useRef)(null); const [size, setSize] = (0, import_react22.useState)(null); const updateSize = (0, import_react22.useCallback)((entries) => { const entry = entries[0]; if (!entry) return; const { width, height, top, left, bottom, right } = entry.contentRect; setSize({ width, height, top, left, bottom, right }); }, []); (0, import_react22.useEffect)(() => { const element = ref.current; if (!element || typeof ResizeObserver === "undefined") return; const rect = element.getBoundingClientRect(); setSize({ width: rect.width, height: rect.height, top: rect.top, left: rect.left, bottom: rect.bottom, right: rect.right }); const observer = new ResizeObserver(updateSize); observer.observe(element); return () => observer.disconnect(); }, [updateSize]); return { ref, size }; } // src/hooks/dom/useWindowSize.ts var import_react23 = require("react"); function useWindowSize() { const isSSR = typeof window === "undefined"; const [size, setSize] = (0, import_react23.useState)({ width: isSSR ? 0 : window.innerWidth, height: isSSR ? 0 : window.innerHeight }); const updateSize = (0, import_react23.useCallback)(() => { setSize({ width: window.innerWidth, height: window.innerHeight }); }, []); (0, import_react23.useLayoutEffect)(() => { if (isSSR) return; updateSize(); window.addEventListener("resize", updateSize); return () => { window.removeEventListener("resize", updateSize); }; }, [isSSR, updateSize]); return size; } // src/hooks/location/useGeoLocation.ts var import_react24 = require("react"); function useGeoLocation(options = {}) { const { enableHighAccuracy = false, maximumAge = 0, timeout = 1e4, retryLimit = 3, retryDelay = 2e3 } = options; const [loading, setLoading] = (0, import_react24.useState)(true); const [error, setError] = (0, import_react24.useState)(null); const [coords, setCoords] = (0, import_react24.useState)(null); const retriesRef = (0, import_react24.useRef)(0); const watchIdRef = (0, import_react24.useRef)(null); const retryTimerRef = (0, import_react24.useRef)(null); (0, import_react24.useEffect)(() => { if (typeof navigator === "undefined" || !navigator.geolocation) { setError({ code: 0, message: "Geolocation is not supported by this browser." }); setLoading(false); return; } let cancelled = false; retriesRef.current = 0; const fetchLocation = () => { if (cancelled) return; setLoading(true); const successCallback = (position) => { if (cancelled) return; setCoords(position.coords); setError(null); setLoading(false); retriesRef.current = 0; }; const errorCallback = (positionError) => { if (cancelled) return; setError({ code: positionError.code, message: positionError.message }); if (retriesRef.current < retryLimit) { retriesRef.current += 1; retryTimerRef.current = setTimeout(fetchLocation, retryDelay); } else { setLoading(false); } }; watchIdRef.current = navigator.geolocation.watchPosition( successCallback, errorCallback, { enableHighAccuracy, maximumAge, timeout } ); }; fetchLocation(); return () => { cancelled = true; if (watchIdRef.current !== null) { navigator.geolocation.clearWatch(watchIdRef.current); watchIdRef.current = null; } if (retryTimerRef.current !== null) { clearTimeout(retryTimerRef.current); retryTimerRef.current = null; } }; }, [enableHighAccuracy, maximumAge, timeout, retryLimit, retryDelay]); return { loading, error, coords }; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { useAdvancedEffect, useArray, useClickOutside, useCopyToClipboard, useCounter, useDebounce, useEventListener, useFormState, useGeoLocation, useHistory, useHover, useInterval, useLocalStorage, useOnScreen, useOnlineStatus, usePress, usePrevious, useScrollInfo, useSessionStorage, useSize, useStorage, useTimeout, useToggle, useUpdatedEffect, useWindowSize });