UNPKG

@hyunjin/hooks

Version:

collection of useful hooks

536 lines (508 loc) 15.1 kB
// src/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.ts import { useEffect, useLayoutEffect } from "react"; var canUseDOM = !!(typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined"); var useIsomorphicLayoutEffect = canUseDOM ? useLayoutEffect : useEffect; // src/useBodyScrollLock/useBodyScrollLock.ts var useBodyScrollLock = (enable = true) => { useIsomorphicLayoutEffect(() => { if (enable === false) { return; } const originalStyle = window.getComputedStyle(document.body).overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = originalStyle; }; }, [enable]); }; // src/useBoolean/useBoolean.ts import { useCallback, useState } from "react"; var useBoolean = (defaultValue = false) => { if (typeof defaultValue !== "boolean") { throw new Error("defaultValue must be `true` or `false`"); } const [value, setValue] = useState(defaultValue); const setTrue = useCallback(() => { setValue(true); }, []); const setFalse = useCallback(() => { setValue(false); }, []); const toggle = useCallback(() => { setValue((x) => !x); }, []); return { value, setValue, setTrue, setFalse, toggle }; }; // src/useCallbackOnce/useCallbackOnce.ts import { useCallback as useCallback2, useRef } from "react"; var useCallbackOnce = (callback, deps) => { const hasFired = useRef(false); const memoizedCallback = useCallback2((...args) => { if (hasFired.current) { return; } callback(...args); hasFired.current = true; }, deps); return memoizedCallback; }; // src/useCopyToClipboard/useCopyToClipboard.ts import { useCallback as useCallback3, useState as useState2 } from "react"; var useCopyToClipboard = () => { const [copiedText, setCopiedText] = useState2(null); const copy = useCallback3(async (text) => { if (!(navigator == null ? void 0 : navigator.clipboard)) { console.warn("Clipboard not supported"); return false; } try { await navigator.clipboard.writeText(text); setCopiedText(text); return true; } catch (error) { console.warn("Copy failed", error); setCopiedText(null); return false; } }, []); return { copiedText, copy }; }; // src/useDebounce/useDebounce.ts import { useEffect as useEffect2, useState as useState3 } from "react"; var useDebounce = (value, delay) => { const [debouncedValue, setDebouncedValue] = useState3(value); useEffect2(() => { const timer = setTimeout(() => setDebouncedValue(value), delay || 500); return () => { clearTimeout(timer); }; }, [value, delay]); return debouncedValue; }; // src/useDocumentTitle/useDocumentTitle.ts var useDocumentTitle = (title) => { useIsomorphicLayoutEffect(() => { window.document.title = title; }, [title]); }; // src/useEventListener/useEventListener.ts import { useEffect as useEffect3, useRef as useRef2 } from "react"; var useEventListener = (eventName, handler, element = globalThis, options = {}) => { const savedHandler = useRef2(handler); useEffect3(() => { savedHandler.current = handler; }, [handler]); useEffect3(() => { const isSupported = element && "addEventListener" in element; if (!isSupported) { return; } const eventListener = (event) => { if (savedHandler.current) { savedHandler.current(event); } }; const { capture, passive, once } = options; const opts = { capture, passive, once }; element.addEventListener(eventName, eventListener, opts); return () => { element.removeEventListener(eventName, eventListener, opts); }; }, [eventName, element, options.capture, options.passive, options.once]); }; // src/useFetch/useFetch.ts import { useEffect as useEffect4, useReducer, useRef as useRef3 } from "react"; function useFetch(url, options) { const cache = useRef3({}); const cancelRequest = useRef3(false); const initialState = { error: void 0, data: void 0 }; const fetchReducer = (state2, action) => { switch (action.type) { case "loading": return { ...initialState }; case "fetched": return { ...initialState, data: action.payload }; case "error": return { ...initialState, error: action.payload }; default: return state2; } }; const [state, dispatch] = useReducer(fetchReducer, initialState); useEffect4(() => { if (!url) { return; } cancelRequest.current = false; const fetchData = async () => { dispatch({ type: "loading" }); if (cache.current[url]) { dispatch({ type: "fetched", payload: cache.current[url] }); return; } try { const response = await fetch(url, options); if (!response.ok) { throw new Error(response.statusText); } const data = await response.json(); cache.current[url] = data; if (cancelRequest.current) { return; } dispatch({ type: "fetched", payload: data }); } catch (error) { if (cancelRequest.current) { return; } dispatch({ type: "error", payload: error }); } }; void fetchData(); return () => { cancelRequest.current = true; }; }, [url]); return state; } // src/useForceUpdate/useForceUpdate.ts import { useReducer as useReducer2 } from "react"; var updater = (num) => (num + 1) % 1e6; var useForceUpdate = () => { const [, forceUpdate] = useReducer2(updater, 0); return forceUpdate; }; // src/useInterval/useInterval.ts import { useEffect as useEffect5, useRef as useRef4 } from "react"; var useInterval = (callback, delay) => { const savedCallbackRef = useRef4(callback); useEffect5(() => { savedCallbackRef.current = callback; }, [callback]); useEffect5(() => { const handler = (...args) => { var _a; return (_a = savedCallbackRef.current) == null ? void 0 : _a.call(savedCallbackRef, ...args); }; if (delay) { const id = setInterval(handler, delay); return () => clearInterval(id); } }, [delay]); }; // src/useIsClient/useIsClient.ts import { useEffect as useEffect6, useState as useState4 } from "react"; var useIsClient = () => { const [isClient, setClient] = useState4(false); useEffect6(() => { setClient(true); }, []); return isClient; }; // src/useIsFirstRender/useIsFirstRender.ts import { useRef as useRef5 } from "react"; var useIsFirstRender = () => { const isFirst = useRef5(true); if (isFirst.current) { isFirst.current = false; return true; } return isFirst.current; }; // src/useIsMounted/useIsMounted.ts import { useCallback as useCallback4, useEffect as useEffect7, useRef as useRef6 } from "react"; var useIsMounted = () => { const isMounted = useRef6(false); useEffect7(() => { isMounted.current = true; return () => { isMounted.current = false; }; }, []); return useCallback4(() => isMounted.current, []); }; // src/useIsScrolled/useIsScrolled.ts import { useEffect as useEffect8, useState as useState5 } from "react"; var useIsScrolled = (threshold = 50) => { const [isScrolled, setIsScrolled] = useState5(false); useEffect8(() => { const handleScroll = () => { const scrolled = window.scrollY > threshold; setIsScrolled(scrolled); }; window.addEventListener("scroll", handleScroll); return () => { window.removeEventListener("scroll", handleScroll); }; }, [threshold]); return isScrolled; }; // src/useKeyPress/useKeypress.ts import { useEffect as useEffect9 } from "react"; var useKeyPress = ({ useAltKey = true, active = true, targetCode, onPress, dep = [] }) => { useEffect9(() => { const targetCodesArray = Array.isArray(targetCode) ? targetCode : [targetCode]; const downHandler = ({ code, altKey }) => { if (altKey === useAltKey && targetCodesArray.includes(code) && active) { onPress(); } }; window.addEventListener("keydown", downHandler); return () => { window.removeEventListener("keydown", downHandler); }; }, [active, useAltKey, onPress, targetCode, ...dep]); }; // src/useLatestValue/useLatestValue.ts import { useRef as useRef7 } from "react"; var useLatestValue = (value) => { const cache = useRef7(value); useIsomorphicLayoutEffect(() => { cache.current = value; }, [value]); return cache; }; // src/useLockScroll/useLockScroll.ts var useLockScroll = (isOpen = true) => { useIsomorphicLayoutEffect(() => { if (isOpen) { document.body.style.overflow = "hidden"; } else { document.body.style.overflow = "unset"; } return () => { document.body.style.overflow = "unset"; }; }, [isOpen]); }; // src/useMount/useMount.ts import { useEffect as useEffect10 } from "react"; var useMount = (callback) => { useEffect10(callback, []); }; // src/useOnClickOutside/useOnClickOutside.ts import { useEffect as useEffect11 } from "react"; var useOnClickOutside = (ref, handler) => { useEffect11(() => { const listener = (event) => { if (!ref.current || ref.current.contains(event.target)) { return; } handler(event); }; document.addEventListener("mousedown", listener); document.addEventListener("touchstart", listener); return () => { document.removeEventListener("mousedown", listener); document.removeEventListener("touchstart", listener); }; }, [ref, handler]); }; // src/useOnlineStatus/useOnlineStatus.ts import { useSyncExternalStore } from "react"; var getNetworkState = () => { return navigator.onLine; }; var subscribe = (callback) => { window.addEventListener("online", callback); window.addEventListener("offline", callback); return () => { window.removeEventListener("online", callback); window.removeEventListener("offline", callback); }; }; var getServerSnapshot = () => { return true; }; var useOnlineStatus = () => { const isOnline = useSyncExternalStore( subscribe, getNetworkState, getServerSnapshot ); return isOnline; }; // src/useOverlay/OverlayContext.tsx import { useContext, useState as useState6, useMemo, useCallback as useCallback5, createContext, Fragment } from "react"; import { jsx, jsxs } from "react/jsx-runtime"; var OverlayContext = createContext(null); var OverlayProvider = ({ children }) => { const [overlays, setOverlays] = useState6(/* @__PURE__ */ new Map()); const openOverlay = useCallback5((id, element) => { setOverlays((prev) => { const cloned = new Map(prev); cloned.set(id, element); return cloned; }); }, []); const closeOverlay = useCallback5((id) => { setOverlays((prev) => { const cloned = new Map(prev); cloned.delete(id); return cloned; }); }, []); const contextValue = useMemo( () => ({ openOverlay, closeOverlay }), [openOverlay, closeOverlay] ); return /* @__PURE__ */ jsxs(OverlayContext.Provider, { value: contextValue, children: [ children, /* @__PURE__ */ jsx("div", { id: "overlay-container", children: [...overlays.entries()].map(([key, element]) => /* @__PURE__ */ jsx(Fragment, { children: element }, key)) }) ] }); }; var useOverlayContext = () => { const ctx = useContext(OverlayContext); if (!ctx) throw new Error( "Cannot find OverlayContext. It should be wrapped within OverlayProvider." ); return ctx; }; // src/useOverlay/useOverlay.ts import { useState as useState7, useCallback as useCallback6 } from "react"; var overlayId = 1; var useOverlay = () => { const { openOverlay, closeOverlay } = useOverlayContext(); const [id] = useState7(() => String(overlayId++)); const open = useCallback6( (element) => openOverlay(id, element), [openOverlay, id] ); const close = useCallback6(() => closeOverlay(id), [closeOverlay, id]); return { open, close }; }; // src/useSafeContext/useSafeContext.ts import { useContext as useContext2 } from "react"; var useSafeContext = (unsafeContext, customMessage) => { const context = useContext2(unsafeContext); if (!context) { const displayName = unsafeContext.displayName; let errorMessage; switch (typeof customMessage) { case "string": errorMessage = customMessage; break; case "function": errorMessage = customMessage(displayName); break; default: { const contextName = displayName || "a context"; errorMessage = `You are trying to use ${contextName} outside of the provider`; break; } } throw new Error(errorMessage); } return context; }; // src/useToggle/useToggle.ts import { useCallback as useCallback7, useState as useState8 } from "react"; var useToggle = (defaultValue) => { const [value, setValue] = useState8(!!defaultValue); const toggle = useCallback7(() => setValue((prev) => !prev), []); return [value, toggle, setValue]; }; // src/useUpdateEffect/useUpdateEffect.ts import { useEffect as useEffect12 } from "react"; var useUpdateEffect = (effect, deps) => { const isFirst = useIsFirstRender(); useEffect12(() => { if (!isFirst) { return effect(); } }, deps); }; // src/useUpload/useUpload.ts import { useState as useState9 } from "react"; var useUpload = () => { const [file, setFile] = useState9(); const upload = () => { const promise = new Promise((resolve, reject) => { const input = document.createElement("input"); input.accept = "image/*"; const timeout = setTimeout(reject, 1e3 * 60 * 3); input.type = "file"; input.onchange = () => { clearTimeout(timeout); if (!input.files) { return reject(); } const file2 = input.files[0]; setFile(file2); resolve(file2); }; input.click(); }); return promise; }; return { upload, file }; }; // src/useWindowSize/useWindowSize.ts import { useEffect as useEffect13, useState as useState10 } from "react"; var useWindowSize = () => { const [windowSize, setWindowSize] = useState10(); useEffect13(() => { const handleResize = () => { setWindowSize({ width: window.innerWidth, height: window.innerHeight }); }; window.addEventListener("resize", handleResize); handleResize(); return () => window.removeEventListener("resize", handleResize); }, []); return windowSize; }; export { OverlayProvider, canUseDOM, useBodyScrollLock, useBoolean, useCallbackOnce, useCopyToClipboard, useDebounce, useDocumentTitle, useEventListener, useFetch, useForceUpdate, useInterval, useIsClient, useIsFirstRender, useIsMounted, useIsScrolled, useIsomorphicLayoutEffect, useKeyPress, useLatestValue, useLockScroll, useMount, useOnClickOutside, useOnlineStatus, useOverlay, useOverlayContext, useSafeContext, useToggle, useUpdateEffect, useUpload, useWindowSize };