iuseful-react-hooks
Version:
'Use'-ful React hooks
816 lines (779 loc) • 24.3 kB
JavaScript
// 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
};