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.

659 lines (635 loc) 19.7 kB
// src/hooks/default/useAdvancedEffect.ts import { useEffect, useRef } from "react"; function useAdvancedEffect(effect, deps) { const firstRender = useRef(true); const previousDepsRef = useRef(void 0); useEffect(() => { if (firstRender.current) { firstRender.current = false; previousDepsRef.current = deps; return; } const hasDepsChanged = deps.some( (dep, i) => dep !== previousDepsRef.current?.[i] ); if (hasDepsChanged) { previousDepsRef.current = deps; return effect(); } }, [deps, effect]); } // src/hooks/default/useUpdatedEffect.ts import { useEffect as useEffect2, useRef as useRef2 } from "react"; function useUpdatedEffect(effect, deps) { const previousDepsRef = useRef2(void 0); useEffect2(() => { previousDepsRef.current = deps; const isDepsChanged = deps.some( (dep, i) => dep !== previousDepsRef.current?.[i] ); if (isDepsChanged) { previousDepsRef.current = deps; return effect(); } }, [effect, deps]); } // src/hooks/state-management/useArray.ts import { useRef as useRef3, useState } from "react"; function useArray(initialValue) { const [state, setState] = useState(initialValue); const initialValueRef = useRef3(initialValue); const push = (value) => { setState((prev) => [...prev, value]); return state.length + 1; }; const pop = () => { const lastElement = state[state.length - 1]; setState((prev) => prev.slice(0, -1)); return lastElement; }; const unshift = (value) => { setState((prev) => [value, ...prev]); return state.length + 1; }; const shift = () => { const firstElement = state[0]; setState((prev) => prev.slice(1)); return firstElement; }; const removeByIndex = (index) => { setState((prev) => [...prev.slice(0, index), ...prev.slice(index + 1)]); }; const removeByValue = (value) => { setState( (prev) => prev.filter((item) => { if (typeof value === "object") { return JSON.stringify(item) !== JSON.stringify(value); } else { return item !== value; } }) ); }; const clear = () => { setState([]); }; const replace = (newArray) => { setState(newArray); }; const reset = () => { setState(initialValueRef.current); }; const filter = (predicate) => { setState((prev) => prev.filter(predicate)); }; const updateByIndex = (index, value) => { setState((prev) => prev.map((item, i) => i === index ? value : item)); }; const updateByValue = (prevValue, newValue) => { setState( (prev) => prev.map((item) => { if (typeof item !== "object") return item === prevValue ? newValue : item; return JSON.stringify(item) === JSON.stringify(prevValue) ? newValue : item; }) ); }; return [ state, setState, { push, pop, shift, unshift, removeByIndex, removeByValue, clear, filter, reset, replace, updateByIndex, updateByValue } ]; } // src/hooks/state-management/useCounter.ts import { useRef as useRef4, useState as useState2 } from "react"; function useCounter(initialValue = 0) { const [count, setCount] = useState2(initialValue); const initialValueRef = useRef4(initialValue); const increment = () => { setCount((prev) => prev + 1); }; const incrementByValue = (value) => { setCount((prev) => prev + value); }; const decrement = () => { setCount((prev) => prev - 1); }; const decrementByValue = (value) => { setCount((prev) => prev - value); }; const reset = () => { setCount(initialValueRef.current); }; return { count, increment, incrementByValue, decrement, decrementByValue, reset }; } // src/hooks/state-management/useFormState.ts import { useCallback, useMemo, useRef as useRef5, useState as useState3 } from "react"; function useFormState(defaultValue, predicates, { emptyInputValidation = true } = {}) { const resolvedDefaultValue = useMemo( () => typeof defaultValue === "function" ? defaultValue() : defaultValue, [defaultValue] ); const resolvedDefaultValueRef = useRef5(resolvedDefaultValue); const [state, setState] = useState3(resolvedDefaultValue); const [errors, setErrors] = useState3( Array(predicates.length).fill(void 0) ); const validate = useCallback( (value) => { const newErrors = predicates.map((predicate) => predicate(value)); if (JSON.stringify(newErrors) !== JSON.stringify(errors)) { setErrors(newErrors); } }, [predicates, errors] ); const setValue = useCallback( (value) => { setState((prev) => { const newValue = typeof value === "function" ? value(prev) : value; validate(newValue); if (typeof newValue === "string" && newValue.length === 0 && !emptyInputValidation) { setErrors([]); } return newValue; }); }, [validate, emptyInputValidation] ); const filteredErrors = useMemo( () => errors.filter((error) => !!error), [errors] ); const isValid = filteredErrors.length === 0; const status = 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 import { useCallback as useCallback2, useRef as useRef6, useState as useState4 } from "react"; function useHistory(defaultValue, { capacity = 10 } = {}) { const resolvedDefaultValue = typeof defaultValue === "function" ? defaultValue() : defaultValue; const [state, setState] = useState4(resolvedDefaultValue); const historyRef = useRef6([resolvedDefaultValue]); const pointerRef = useRef6(0); const set = useCallback2( (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 = useCallback2(() => { if (pointerRef.current > 0) { pointerRef.current--; setState(historyRef.current[pointerRef.current]); } }, []); const forward = useCallback2(() => { if (pointerRef.current < historyRef.current.length - 1) { pointerRef.current++; setState(historyRef.current[pointerRef.current]); } }, []); const go = useCallback2((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 import { useEffect as useEffect3, useRef as useRef7 } from "react"; function usePrevious(value) { const ref = useRef7(null); useEffect3(() => { ref.current = value; }, [value]); return ref.current; } // src/hooks/state-management/useToggle.ts import { useState as useState5 } from "react"; function useToggle(initialValue) { const [state, setState] = useState5(initialValue); function setValue(value) { setState((prev) => typeof value === "boolean" ? value : !prev); } return [state, setValue]; } // src/hooks/async-management/useDebounce.ts import { useEffect as useEffect5 } from "react"; // src/hooks/async-management/useTimeout.ts import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef8 } from "react"; function useTimeout(callback, delay) { const callbackRef = useRef8(callback); const timeoutRef = useRef8(); const set = useCallback3(() => { timeoutRef.current = window.setTimeout(() => { callbackRef.current(); }, delay); }, [delay]); const clear = useCallback3(() => { if (timeoutRef.current !== void 0) { clearTimeout(timeoutRef.current); timeoutRef.current = void 0; } }, []); const reset = useCallback3(() => { clear(); set(); }, [clear, set]); useEffect4(() => { set(); return clear; }, [set, clear]); useEffect4(() => { callbackRef.current = callback; }, [callback]); return { set, clear, reset }; } // src/hooks/async-management/useDebounce.ts function useDebounce(callback, delay, deps) { const { reset, clear } = useTimeout(callback, delay); useEffect5(reset, [...deps, reset]); useEffect5(clear, [clear]); } // src/hooks/async-management/useInterval.ts import { useEffect as useEffect6, useRef as useRef9 } from "react"; function useInterval(callback, interval = 1e3) { const intervalRef = useRef9(null); const clear = () => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }; useEffect6(() => { intervalRef.current = setInterval(callback, interval); return clear; }, [callback, interval]); return { clear }; } // src/hooks/storage/useStorage.ts import { useEffect as useEffect7, useState as useState6 } from "react"; function useStorage(key, defaultValue, storage) { const [value, setValue] = useState6(() => { try { const storedJsonValue = storage.getItem(key); if (storedJsonValue !== null) return JSON.parse(storedJsonValue); } catch (error) { console.error(`Error parsing storage key "${key}":`, error); } return typeof defaultValue === "function" ? defaultValue() : defaultValue; }); useEffect7(() => { if (value === void 0) { storage.removeItem(key); } else { storage.setItem(key, JSON.stringify(value)); } }, [value, key, storage]); return [value, setValue]; } function useLocalStorage(key, defaultValue) { return useStorage(key, defaultValue, window.localStorage); } function useSessionStorage(key, defaultValue) { return useStorage(key, defaultValue, window.sessionStorage); } // src/hooks/dom/useCopyToClipboard.ts import { useState as useState7 } from "react"; function useCopyToClipboard() { const [isCopied, setIsCopied] = useState7(false); const [error, setError] = useState7(null); async function copy(text) { try { if (!navigator.clipboard) { throw new Error("Clipboard API not available"); } await navigator.clipboard.writeText(text); setIsCopied(true); setError(null); setTimeout(() => setIsCopied(false), 2e3); } catch (err) { setError(err.message); setIsCopied(false); } } return { copy, isCopied, error }; } // src/hooks/dom/useEventListener.ts import { useRef as useRef10, useEffect as useEffect8 } from "react"; function useEventListener(eventType, callback, elementRef, options) { const callbackRef = useRef10(callback); useEffect8(() => { callbackRef.current = callback; }, [callback]); useEffect8(() => { const target = elementRef?.current ?? window; if (!(target && target.addEventListener)) return; const handleEvent = (event) => callbackRef.current(event); target.addEventListener(eventType, handleEvent, options); return () => { target.removeEventListener( eventType, handleEvent, options ); }; }, [eventType, elementRef, options]); } // src/hooks/dom/useHover.ts import { useRef as useRef11, useState as useState8, useCallback as useCallback4 } from "react"; function useHover() { const [isHovered, setIsHovered] = useState8(false); const ref = useRef11(null); const handleHover = useCallback4( (event) => setIsHovered(event.type === "mouseenter"), [] ); useEventListener("mouseenter", handleHover, ref); useEventListener("mouseleave", handleHover, ref); return { ref, isHovered }; } // src/hooks/dom/useOnClickOutside.ts import { useCallback as useCallback5, useRef as useRef12 } from "react"; function useClickOutside(callback) { const ref = useRef12(null); const handleClick = useCallback5( (event) => { if (!ref.current?.contains(event.target)) { callback(); } }, [callback, ref] ); useEventListener("mousedown", handleClick, { current: document.body }); useEventListener("touchstart", handleClick, { current: document.body }); return { ref }; } // src/hooks/dom/useOnlineStatus.ts import { useState as useState9 } from "react"; function useOnlineStatus() { const [onlineStatus, setOnlineStatus] = useState9( navigator.onLine ? "online" : "offline" ); const updateStatus = () => { setOnlineStatus(navigator.onLine ? "online" : "offline"); }; useEventListener("online", updateStatus, { current: window }); return { onlineStatus }; } // src/hooks/dom/useOnScreen.ts import { useEffect as useEffect9, useRef as useRef13, useState as useState10 } from "react"; function useOnScreen(rootMargin = "0px") { const [isVisible, setIsVisible] = useState10(false); const ref = useRef13(null); useEffect9(() => { const target = ref.current; if (!target) return; const observer = new IntersectionObserver( ([entry]) => setIsVisible(entry.isIntersecting), { rootMargin } ); observer.observe(target); return () => observer.unobserve(target); }, [ref, rootMargin]); return { ref, isVisible }; } // src/hooks/dom/usePress.ts import { useRef as useRef14, useState as useState11 } from "react"; function usePress() { const [isPressed, setIsPressed] = useState11(false); const ref = useRef14(null); useEventListener("mousedown", () => setIsPressed(true), ref); useEventListener("mouseup", () => setIsPressed(false), ref); return { isPressed, ref }; } // src/hooks/dom/useScrollPosition.ts import { useState as useState12, useEffect as useEffect10, useRef as useRef15, useCallback as useCallback6 } from "react"; function useScrollInfo() { const ref = useRef15(null); const [scrollData, setScrollData] = useState12({ scrollX: 0, scrollY: 0, scrollDirection: "none", isScrolling: false, scrollProgress: 0 }); const lastScrollY = useRef15(0); const lastScrollX = useRef15(0); const scrollTimeout = useRef15(null); const handleScroll = useCallback6(() => { const target = ref.current ?? document.documentElement; const newScrollX = target.scrollLeft ?? window.scrollX; const newScrollY = target.scrollTop ?? window.scrollY; const maxScrollHeight = target.scrollHeight - target.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); }, []); useEffect10(() => { const target = ref.current ?? window; target.addEventListener("scroll", handleScroll, { passive: true }); return () => target.removeEventListener("scroll", handleScroll); }, [handleScroll]); return { ref, ...scrollData }; } // src/hooks/dom/useSize.ts import { useEffect as useEffect11, useRef as useRef16, useState as useState13, useCallback as useCallback7 } from "react"; function useSize() { const ref = useRef16(null); const [size, setSize] = useState13(null); const updateSize = useCallback7((entries) => { const entry = entries[0]; if (!entry) return; const { width, height, top, left, bottom, right } = entry.contentRect; setSize({ width, height, top, left, bottom, right }); }, []); useEffect11(() => { const element = ref.current; if (!element) return; const observer = new ResizeObserver(updateSize); observer.observe(element); return () => observer.disconnect(); }, [updateSize]); return { ref, size }; } // src/hooks/dom/useWindowSize.ts import { useState as useState14, useCallback as useCallback8, useLayoutEffect } from "react"; function useWindowSize() { const isSSR = typeof window === "undefined"; const [size, setSize] = useState14({ width: isSSR ? 0 : window.innerWidth, height: isSSR ? 0 : window.innerHeight }); const updateSize = useCallback8(() => { setSize({ width: window.innerWidth, height: window.innerHeight }); }, []); useLayoutEffect(() => { if (isSSR) return; updateSize(); window.addEventListener("resize", updateSize); return () => { window.removeEventListener("resize", updateSize); }; }, [isSSR, updateSize]); return size; } // src/hooks/location/useGeoLocation.ts import { useEffect as useEffect12, useState as useState15, useRef as useRef17 } from "react"; function useGeoLocation(options) { const [loading, setLoading] = useState15(true); const [error, setError] = useState15(null); const [coords, setCoords] = useState15(null); const retriesRef = useRef17(0); const watchIdRef = useRef17(null); useEffect12(() => { if (!navigator.geolocation) { setError({ code: 0, message: "Geolocation is not supported by this browser." }); setLoading(false); return; } const { enableHighAccuracy = false, maximumAge = 0, timeout = 1e4, retryLimit = 3, retryDelay = 2e3 } = options || {}; const fetchLocation = () => { setLoading(true); const successCallback = (position) => { setCoords(position.coords); setError(null); setLoading(false); retriesRef.current = 0; }; const errorCallback = (positionError) => { setError({ code: positionError.code, message: positionError.message }); if (retriesRef.current < retryLimit) { retriesRef.current += 1; setTimeout(fetchLocation, retryDelay); } else { setLoading(false); } }; watchIdRef.current = navigator.geolocation.watchPosition( successCallback, errorCallback, { enableHighAccuracy, maximumAge, timeout } ); }; fetchLocation(); return () => { if (watchIdRef.current !== null) { navigator.geolocation.clearWatch(watchIdRef.current); } }; }, [options]); return { loading, error, coords }; } export { 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 };