UNPKG

iuseful-react-hooks

Version:
816 lines (779 loc) 24.3 kB
// src/hooks/use-boolean.ts import { useCallback, useState } from "react"; function useBoolean(initialValue = false) { const [value, setValue] = useState(initialValue); const toggle = useCallback(() => { setValue((prev) => !prev); }, []); const set = useCallback((param) => { setValue(param); }, []); return { set, toggle, value }; } // src/hooks/use-broadcast-channel-sender.ts import { useCallback as useCallback2 } from "react"; // src/hooks/use-singleton-broadcast-channel.ts import { useEffect, useMemo, useRef } from "react"; // src/utils/helpers/is-client.ts var isClient = !!(typeof window !== "undefined" && window.document && window.document.createElement); // src/utils/helpers/broadcast-channels.ts var cache = /* @__PURE__ */ new Map(); function acquireChannel(name) { if (!isClient || typeof BroadcastChannel === "undefined") { return null; } const current = cache.get(name); if (current) { current.count += 1; return current.channel; } const channel = new BroadcastChannel(name); cache.set(name, { channel, count: 1 }); return channel; } function releaseChannel(name, channel) { if (!channel || !isClient) return; const entry = cache.get(name); if (!entry) return; if (entry.channel === channel) { entry.count -= 1; if (entry.count <= 0) { entry.channel.close(); cache.delete(name); } } } // src/hooks/use-singleton-broadcast-channel.ts function useSingletonBroadcastChannel({ name }) { const nameRef = useRef(name); useEffect(() => { nameRef.current = name; }, [name]); const channel = useMemo(() => acquireChannel(name), [name]); useEffect(() => { return () => { releaseChannel(nameRef.current, channel); }; }, [channel]); return { channel }; } // src/hooks/use-broadcast-channel-sender.ts function useBroadcastChannelSender({ channelName }) { const { channel } = useSingletonBroadcastChannel({ name: channelName }); const send = useCallback2( (message) => { if (channel) { channel.postMessage(message); } }, [channel] ); return { send }; } // src/hooks/use-broadcast-channel-listeners.ts import { useCallback as useCallback3, useEffect as useEffect2 } from "react"; function useBroadcastChannelListener({ channelName, onMessage, onError = null }) { const { channel } = useSingletonBroadcastChannel({ name: channelName }); const memoizedOnMessage = useCallback3( (ev) => { onMessage(ev); }, [onMessage] ); const memoizedOnError = useCallback3( (ev) => { onError == null ? void 0 : onError(ev); }, [onError] ); useEffect2(() => { if (!channel) return; channel.addEventListener("message", memoizedOnMessage); if (memoizedOnError) { channel.addEventListener("messageerror", memoizedOnError); } return () => { channel.removeEventListener("message", memoizedOnMessage); if (memoizedOnError) { channel.removeEventListener("messageerror", memoizedOnError); } }; }, [channel]); } // src/hooks/use-is-first-render.ts import { useEffect as useEffect3, useRef as useRef2 } from "react"; function useIsFirstRender() { const isFirstRender = useRef2(true); useEffect3(() => { isFirstRender.current = false; }, []); return isFirstRender.current; } // src/hooks/use-effect-after-mount.ts import { useEffect as useEffect4 } from "react"; function useEffectAfterMount(effect, deps) { const isFirstRender = useIsFirstRender(); useEffect4(() => { if (!isFirstRender) return effect(); }, deps); } // src/hooks/use-near-screen.ts import { useEffect as useEffect5, useRef as useRef3, useState as useState2 } from "react"; function useNearScreen({ externalRef, once = true, rootMargin = "100px" } = {}) { const [isNearScreen, setIsNearScreen] = useState2(false); const fromRef = useRef3(null); useEffect5(() => { if (!isClient || typeof IntersectionObserver === "undefined") return; const element = externalRef ? externalRef.current : fromRef.current; const onChange = ([firstEntry], observer2) => { if (!firstEntry) return; const { isIntersecting } = firstEntry; if (!once || isIntersecting) setIsNearScreen(isIntersecting); if (isIntersecting && once) observer2.disconnect(); }; const observer = new IntersectionObserver(onChange, { rootMargin }); if (element) { observer.observe(element); } return () => { if (observer) { observer.disconnect(); } }; }, [externalRef, once, rootMargin]); return { isNearScreen, fromRef }; } // src/utils/helpers/functions.ts function isPromise(value) { return !!value && (typeof value === "object" || typeof value === "function") && typeof value.then === "function"; } function noop() { } // src/hooks/use-timeout.ts import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef4, useState as useState3 } from "react"; function useTimeout(onTimeout, delayInMs) { const [isCleared, setIsCleared] = useState3(false); const timeoutRef = useRef4(null); const cbRef = useRef4(onTimeout); useEffect6(() => { cbRef.current = onTimeout; }, [onTimeout]); const clear = useCallback4(() => { timeoutRef.current && clearTimeout(timeoutRef.current); timeoutRef.current = null; setIsCleared(true); }, []); const schedule = useCallback4(() => { if (timeoutRef.current != null) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } timeoutRef.current = setTimeout(() => { const result = cbRef.current(); if (isPromise(result)) result.catch(noop); }, delayInMs); setIsCleared(false); }, [delayInMs]); const reset = useCallback4(() => { clear(); schedule(); }, [clear, schedule]); useEffect6(() => { schedule(); return clear; }, [schedule, clear]); return { isCleared, clear, reset }; } // src/hooks/use-debounced-value.ts import { useEffect as useEffect7, useState as useState4 } from "react"; function useDebouncedValue(value, { delayInMs = 300 } = {}) { const [debouncedValue, setDebouncedValue] = useState4(value); useEffect7(() => { const handler = setTimeout(() => { setDebouncedValue(value); }, delayInMs); return () => { clearTimeout(handler); }; }, [value, delayInMs]); return debouncedValue; } // src/hooks/use-debounced-effect.ts import { useEffect as useEffect8 } from "react"; function useDebouncedEffect(callback, delayInMs = 150, deps = [], { immediate = true } = {}) { const isFirstRender = useIsFirstRender(); const { reset, clear } = useTimeout(callback, delayInMs); useEffect8(() => { if (isFirstRender && immediate) { callback(); } reset(); }, [...deps, reset]); useEffect8(clear, []); } // src/hooks/use-array.ts import { useCallback as useCallback5, useState as useState5 } from "react"; function useArray(initialValue) { const [array, setArray] = useState5(initialValue); const set = useCallback5((newArray) => { setArray(newArray); }, []); const push = useCallback5((item) => { setArray((prevArray) => [...prevArray, item]); }, []); const pop = useCallback5(() => { setArray((prevArray) => prevArray.slice(0, -1)); }, []); const remove = useCallback5((index) => { setArray((prevArray) => prevArray.filter((_, i) => i !== index)); }, []); const clear = useCallback5(() => { setArray([]); }, []); const update = useCallback5((index, value) => { setArray((prevArray) => prevArray.with(index, value)); }, []); const filter = useCallback5((...args) => { setArray((prevArray) => prevArray.filter(...args)); }, []); const concat = useCallback5((array2) => { setArray((prevArray) => [...prevArray, ...array2]); }, []); const merge = useCallback5((...arrays) => { setArray((prevArray) => [...prevArray, ...arrays.flat()]); }, []); const shift = useCallback5(() => { setArray((prevArray) => prevArray.slice(1)); }, []); const sort = useCallback5((compareFn) => { setArray((prevArray) => prevArray.toSorted(compareFn)); }, []); return { array, set, push, pop, remove, clear, update, filter, concat, merge, shift, sort }; } // src/hooks/use-previous.ts import { useRef as useRef5 } from "react"; function usePrevious(value) { const prevRef = useRef5(null); const currentRef = useRef5(value); if (currentRef.current !== value) { prevRef.current = currentRef.current; currentRef.current = value; } return prevRef.current; } // src/hooks/use-state-with-history.ts import { useCallback as useCallback6, useRef as useRef6, useState as useState6 } from "react"; function useStateWithHistory(initialState, { historyLimit = 10 } = {}) { const [value, setValue] = useState6(initialState); const historyRef = useRef6([value]); const pointerRef = useRef6(0); const set = useCallback6( (newValue) => { const isUpdateFunction = typeof newValue === "function" && newValue.length === 1; const resolvedValue = isUpdateFunction ? newValue(value) : newValue; if (historyRef.current[pointerRef.current] === resolvedValue) return; if (pointerRef.current < historyRef.current.length - 1) { historyRef.current.splice(pointerRef.current + 1); } historyRef.current.push(resolvedValue); while (historyRef.current.length > historyLimit) { historyRef.current.shift(); } pointerRef.current = historyRef.current.length - 1; setValue(resolvedValue); }, [value, historyLimit] ); const go = useCallback6((pointer) => { if (pointer < 0 || pointer >= historyRef.current.length) return; pointerRef.current = pointer; setValue(historyRef.current[pointer]); }, []); const back = useCallback6(() => { if (pointerRef.current <= 0) return; pointerRef.current--; setValue(historyRef.current[pointerRef.current]); }, []); const forward = useCallback6(() => { if (pointerRef.current >= historyRef.current.length - 1) return; pointerRef.current++; setValue(historyRef.current[pointerRef.current]); }, []); return [ value, set, { history: historyRef.current, pointer: pointerRef.current, go, back, forward } ]; } // src/hooks/use-storage.ts import { useCallback as useCallback7, useState as useState7, useEffect as useEffect9 } from "react"; function useLocalStorage(key, defaultValue) { return useStorage(key, defaultValue, isClient ? window.localStorage : null); } function useSessionStorage(key, defaultValue) { return useStorage(key, defaultValue, isClient ? window.sessionStorage : null); } function useStorage(key, defaultValue, storageObject) { const [value, setValue] = useState7(() => { if (!storageObject) { return typeof defaultValue === "function" ? defaultValue() : defaultValue; } const jsonValue = storageObject.getItem(key); if (jsonValue != null) return JSON.parse(jsonValue); return typeof defaultValue === "function" ? defaultValue() : defaultValue; }); useEffect9(() => { if (!storageObject) return; if (value === void 0) return storageObject.removeItem(key); storageObject.setItem(key, JSON.stringify(value)); }, [key, value, storageObject]); const remove = useCallback7(() => { setValue(void 0); }, []); return [value, setValue, remove]; } // src/hooks/use-async.ts import { useCallback as useCallback8, useEffect as useEffect10, useState as useState8 } from "react"; function useAsync(cb, dependencies = []) { const [value, setValue] = useState8(null); const [error, setError] = useState8(null); const [loading, setLoading] = useState8(true); const refresh = useCallback8(() => { setLoading(true); setValue(null); setError(null); cb().then(setValue).catch(setError).finally(() => setLoading(false)); }, dependencies); useEffect10(refresh, [refresh]); return { value, error, loading, refresh, setValue }; } // src/hooks/use-script.ts function useScript(url) { return useAsync(() => { if (!isClient) { return Promise.resolve(); } const existing = document.querySelector(`script[src="${url}"]`); if (existing) return Promise.resolve(); const script = document.createElement("script"); script.src = url; script.async = true; document.body.appendChild(script); return new Promise((resolve, reject) => { script.onload = resolve; script.onerror = reject; }); }, [url]); } // src/hooks/use-event-listener.ts import { useEffect as useEffect11, useRef as useRef7 } from "react"; function useEventListener(type, listener, target, options) { const listenerRef = useRef7(listener); useEffect11(() => { listenerRef.current = listener; }, [listener]); useEffect11(() => { const resolvedTarget = target != null ? target : isClient ? window : null; if (!resolvedTarget) return; const handler = (event) => { const currentListener = listenerRef.current; if (typeof currentListener === "function") { currentListener.call(resolvedTarget, event); } else if (currentListener && typeof currentListener === "object" && "handleEvent" in currentListener) { currentListener.handleEvent(event); } }; resolvedTarget == null ? void 0 : resolvedTarget.addEventListener(type, handler, options); return () => { resolvedTarget == null ? void 0 : resolvedTarget.removeEventListener(type, handler, options); }; }, [type, target, options]); } // src/hooks/use-window-size.ts import { useState as useState9 } from "react"; var INITIAL_STATE = { width: isClient ? window.innerWidth : 0, height: isClient ? window.innerHeight : 0 }; function useWindowSize() { const [windowSize, setWindowSize] = useState9(INITIAL_STATE); useEventListener("resize", () => { if (isClient) { setWindowSize({ width: window.innerWidth, height: window.innerHeight }); } }); return windowSize; } // src/hooks/use-media-query.ts import { useEffect as useEffect12, useState as useState10 } from "react"; function useMediaQuery(query) { const [isMatch, setIsMatch] = useState10(false); const [mediaQueryList, setMediaQueryList] = useState10(null); useEffect12(() => { if (isClient) { const mql = window.matchMedia(query); setMediaQueryList(mql); setIsMatch(mql.matches); } }, [query]); useEventListener("change", (e) => setIsMatch(e.matches), mediaQueryList); return isMatch; } // src/hooks/use-geolocation.ts import { useState as useState11, useEffect as useEffect13 } from "react"; function useGeolocation(options) { const [loading, setLoading] = useState11(true); const [error, setError] = useState11(null); const [data, setData] = useState11(null); useEffect13(() => { if (!isClient) { setLoading(false); return; } const successHandler = (position) => { setLoading(false); setError(null); setData(position.coords); }; const errorHandler = (error2) => { setError(error2); setLoading(false); }; navigator.geolocation.getCurrentPosition(successHandler, errorHandler, options); const watchId = navigator.geolocation.watchPosition(successHandler, errorHandler, options); return () => navigator.geolocation.clearWatch(watchId); }, [options]); return { loading, error, data }; } // src/hooks/use-state-with-validation.ts import { useCallback as useCallback9, useState as useState12 } from "react"; function useStateWithValidation(validationFn, initialValue) { const [value, setValue] = useState12(initialValue); const [isValid, setIsValid] = useState12(true); const set = useCallback9( (nextValue) => { const resolvedValue = typeof nextValue === "function" ? nextValue(value) : nextValue; setValue(resolvedValue); setIsValid(validationFn(resolvedValue)); }, [validationFn, value] ); return [value, set, isValid]; } // src/hooks/use-size.ts import { useEffect as useEffect14, useRef as useRef8, useState as useState13 } from "react"; function useSize(externalRef) { const fromRef = useRef8(null); const [size, setSize] = useState13({}); useEffect14(() => { var _a; if (!isClient || typeof ResizeObserver === "undefined") return; const element = (_a = externalRef == null ? void 0 : externalRef.current) != null ? _a : fromRef.current; if (!element) return; const observer = new ResizeObserver(([entry]) => setSize(entry.contentRect)); observer.observe(element); return () => observer.disconnect(); }, [externalRef]); if (externalRef) return { size }; return { size, fromRef }; } // src/hooks/use-click-outside.ts import { useRef as useRef9 } from "react"; function useClickOutSide({ onClickOutside, externalRef } = {}) { const fromRef = useRef9(null); useEventListener( "click", (e) => { var _a; const element = (_a = externalRef == null ? void 0 : externalRef.current) != null ? _a : fromRef.current; if (element == null || element.contains(e.target)) return; onClickOutside == null ? void 0 : onClickOutside(e); }, isClient ? document : void 0 ); if (externalRef !== void 0) return; return { fromRef }; } // src/hooks/use-dark-mode.ts import { useEffect as useEffect15 } from "react"; function useDarkMode(darkModeKey = "dark") { const [darkMode, setDarkMode] = useLocalStorage(darkModeKey); const prefersDarkmode = useMediaQuery("(prefers-color-scheme: dark)"); const enabled = darkMode != null ? darkMode : prefersDarkmode; useEffect15(() => { if (isClient) { document.body.classList.toggle(darkModeKey, enabled); } }, [enabled, darkModeKey]); return [enabled, setDarkMode]; } // src/utils/helpers/navigator.ts var copyToClipboard = async (text) => { if (!isClient || !navigator.clipboard) { return Promise.reject(new Error("Clipboard API not available")); } await navigator.clipboard.writeText(text); }; // src/hooks/use-copy-to-clipboard.ts import { useCallback as useCallback10, useState as useState14 } from "react"; function useCopyToClipboard() { const [value, setValue] = useState14(null); const [success, setSuccess] = useState14(null); const [error, setError] = useState14(null); const [isCopying, setIsCopying] = useState14(false); const copy = useCallback10((text) => { setIsCopying(true); setError(null); setValue(null); setSuccess(null); copyToClipboard(text).then(() => { setValue(text); setSuccess(true); }).catch((err) => { setError(err); setSuccess(false); }).finally(() => { setIsCopying(false); }); }, []); return [copy, { value, success, error, isCopying }]; } // src/hooks/use-cookies.ts import { useCallback as useCallback11, useEffect as useEffect16, useRef as useRef10, useState as useState15 } from "react"; // src/utils/helpers/cookies.ts var getCookie = async (cookieName, defaultValue) => { const cookie = await cookieStore.get(cookieName); if (cookie) return cookie; if (defaultValue) { await cookieStore.set({ ...defaultValue, name: cookieName }); return { name: cookieName, value: defaultValue.value }; } return null; }; // src/hooks/use-cookies.ts function useCookie(cookieName, defaultValue) { const { value: cookie, setValue: setCookie, loading: loadingCookie, error: errorLoadingCookie } = useAsync( async () => await getCookie(cookieName, defaultValue) ); const [loadingCookieAction, setLoadingCookieAction] = useState15(false); const [errorOnCookieAction, setErrorOnCookieAction] = useState15(null); const updateCookie = useCallback11( (newValue) => { setLoadingCookieAction(true); cookieStore.set({ ...newValue, name: cookieName }).then( () => setCookie({ name: cookieName, value: newValue.value }) ).catch((error) => setErrorOnCookieAction(error)).finally(() => setLoadingCookieAction(false)); }, [cookieName] ); const deleteCookie = useCallback11(() => { setLoadingCookieAction(true); cookieStore.delete(cookieName).then(() => setCookie(null)).catch((error) => setErrorOnCookieAction(error)).finally(() => setLoadingCookieAction(false)); }, [cookieName]); return [ cookie, { updateCookie, deleteCookie, loadingCookie, errorLoadingCookie, loadingCookieAction, errorOnCookieAction } ]; } var INITIAL_COOKIE_CHANGES = { changed: [], deleted: [] }; function useCookiesChangeListener(onCookieChanges) { const [cookiesChanges, setCookiesChanges] = useState15(INITIAL_COOKIE_CHANGES); const cbRef = useRef10(onCookieChanges); useEffect16(() => { cbRef.current = onCookieChanges; }, [onCookieChanges]); useEventListener( "change", (e) => { setCookiesChanges({ changed: e.changed, deleted: e.deleted }); cbRef.current(e); }, cookieStore ); return cookiesChanges; } function useGetAllCookies(params) { const { value: cookies, loading, error } = useAsync(async () => { if (typeof params === "string") return await cookieStore.getAll(params); return await cookieStore.getAll(params); }); return { cookies, loading, error }; } // src/hooks/use-is-online.ts import { useCallback as useCallback12, useState as useState16 } from "react"; function useIsOnline() { const [isOnline, setIsOnline] = useState16(isClient ? navigator.onLine : true); const handleOnlineStatus = useCallback12(() => { if (isClient) { setIsOnline(navigator.onLine); } }, []); useEventListener("online", handleOnlineStatus); useEventListener("offline", handleOnlineStatus); return isOnline; } // src/hooks/use-long-press.ts import { useEffect as useEffect17, useRef as useRef11 } from "react"; function useLongPress({ externalRef, onLongPress, delay = 300 }) { var _a, _b, _c, _d, _e; const fromRef = useRef11(null); const { reset, clear } = useTimeout(onLongPress, delay); useEffect17(clear, []); useEventListener( "mousedown", reset, (_a = externalRef == null ? void 0 : externalRef.current) != null ? _a : fromRef.current ); useEventListener( "touchstart", reset, (_b = externalRef == null ? void 0 : externalRef.current) != null ? _b : fromRef.current ); useEventListener( "mouseup", clear, (_c = externalRef == null ? void 0 : externalRef.current) != null ? _c : fromRef.current ); useEventListener( "touchend", clear, (_d = externalRef == null ? void 0 : externalRef.current) != null ? _d : fromRef.current ); useEventListener( "mouseleave", clear, (_e = externalRef == null ? void 0 : externalRef.current) != null ? _e : fromRef.current ); if (externalRef == null) return { fromRef }; } // src/hooks/use-hover.ts import { useRef as useRef12 } from "react"; function useHover({ onHover, externalRef, onUnhover } = {}) { const { value: isHovered, set: setIsHovered } = useBoolean(); const fromRef = useRef12(null); const target = externalRef ? externalRef.current : fromRef.current; useEventListener( "mouseenter", (event) => { setIsHovered(true); onHover == null ? void 0 : onHover(event); }, target ); useEventListener( "mouseout", (event) => { setIsHovered(false); onUnhover == null ? void 0 : onUnhover(event); }, target ); if (externalRef === void 0) return { isHovered, fromRef }; return { isHovered }; } export { useArray, useAsync, useBoolean, useBroadcastChannelListener, useBroadcastChannelSender, useClickOutSide, useCookie, useCookiesChangeListener, useCopyToClipboard, useDarkMode, useDebouncedEffect, useDebouncedValue, useEffectAfterMount, useEventListener, useGeolocation, useGetAllCookies, useHover, useIsFirstRender, useIsOnline, useLocalStorage, useLongPress, useMediaQuery, useNearScreen, usePrevious, useScript, useSessionStorage, useSize, useStateWithHistory, useStateWithValidation, useTimeout, useWindowSize };