Repository: uidotdev/usehooks Branch: main Commit: 945436df0037 Files: 104 Total size: 262.1 KB Directory structure: gitextract_24ko9djh/ ├── .gitignore ├── LICENSE ├── README.md ├── index.d.ts ├── index.js ├── package.json ├── tsconfig.json └── usehooks.com/ ├── .astro/ │ └── types.d.ts ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── astro.config.mjs ├── generate-og-images.mjs ├── package.json ├── src/ │ ├── components/ │ │ ├── Button.astro │ │ ├── CodePreview.astro │ │ ├── CountdownTimer.tsx │ │ ├── HookDescription.astro │ │ ├── Install.astro │ │ ├── Logo.astro │ │ ├── LogoGithub.astro │ │ ├── QueryGGBanner.astro │ │ ├── RelatedHook.astro │ │ ├── StaticCodeContainer.astro │ │ ├── Svg.astro │ │ ├── codepreview/ │ │ │ ├── CodePreview.tsx │ │ │ ├── CodeWrapper.tsx │ │ │ └── utils.ts │ │ └── search/ │ │ ├── Callout.module.css │ │ ├── Callout.tsx │ │ ├── HookCard.module.css │ │ ├── HookCard.tsx │ │ ├── HookSearch.module.css │ │ ├── HookSearch.tsx │ │ ├── HookSort.module.css │ │ ├── HookSort.tsx │ │ ├── HooksList.module.css │ │ └── HooksList.tsx │ ├── content/ │ │ ├── config.ts │ │ └── hooks/ │ │ ├── useBattery.mdx │ │ ├── useClickAway.mdx │ │ ├── useContinuousRetry.mdx │ │ ├── useCopyToClipboard.mdx │ │ ├── useCountdown.mdx │ │ ├── useCounter.mdx │ │ ├── useDebounce.mdx │ │ ├── useDefault.mdx │ │ ├── useDocumentTitle.mdx │ │ ├── useEventListener.mdx │ │ ├── useFavicon.mdx │ │ ├── useFetch.mdx │ │ ├── useGeolocation.mdx │ │ ├── useHistoryState.mdx │ │ ├── useHover.mdx │ │ ├── useIdle.mdx │ │ ├── useIntersectionObserver.mdx │ │ ├── useInterval.mdx │ │ ├── useIntervalWhen.mdx │ │ ├── useIsClient.mdx │ │ ├── useIsFirstRender.mdx │ │ ├── useKeyPress.mdx │ │ ├── useList.mdx │ │ ├── useLocalStorage.mdx │ │ ├── useLockBodyScroll.mdx │ │ ├── useLogger.mdx │ │ ├── useLongPress.mdx │ │ ├── useMap.mdx │ │ ├── useMeasure.mdx │ │ ├── useMediaQuery.mdx │ │ ├── useMouse.mdx │ │ ├── useNetworkState.mdx │ │ ├── useObjectState.mdx │ │ ├── useOrientation.mdx │ │ ├── usePageLeave.mdx │ │ ├── usePreferredLanguage.mdx │ │ ├── usePrevious.mdx │ │ ├── useQueue.mdx │ │ ├── useRandomInterval.mdx │ │ ├── useRenderCount.mdx │ │ ├── useRenderInfo.mdx │ │ ├── useScript.mdx │ │ ├── useSessionStorage.mdx │ │ ├── useSet.mdx │ │ ├── useThrottle.mdx │ │ ├── useTimeout.mdx │ │ ├── useToggle.mdx │ │ ├── useVisibilityChange.mdx │ │ ├── useWindowScroll.mdx │ │ └── useWindowSize.mdx │ ├── env.d.ts │ ├── layouts/ │ │ └── Layout.astro │ ├── pages/ │ │ ├── 404.astro │ │ ├── [hook].astro │ │ └── index.astro │ ├── sections/ │ │ ├── Footer.astro │ │ ├── HomeHero.astro │ │ ├── NavInternal.astro │ │ └── NavMain.astro │ └── styles/ │ └── globals.css ├── tailwind.config.cjs ├── theme.json ├── tsconfig.json └── vercel.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Typescript v1 declaration files typings/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # dotenv environment variables file .env # Mac files .DS_Store ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 ui.dev Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ![useHooks](https://usehooks.com/meta/og.jpg) # useHooks A collection of modern, server-safe React hooks – from the [ui.dev](https://ui.dev) team. Compatible with React v18.0.0+. ## Standard ### Install `npm i @uidotdev/usehooks` ### Hooks - [useBattery](https://usehooks.com/usebattery) - [useClickAway](https://usehooks.com/useclickaway) - [useCopyToClipboard](https://usehooks.com/usecopytoclipboard) - [useCounter](https://usehooks.com/usecounter) - [useDebounce](https://usehooks.com/usedebounce) - [useDefault](https://usehooks.com/usedefault) - [useDocumentTitle](https://usehooks.com/usedocumenttitle) - [useFavicon](https://usehooks.com/usefavicon) - [useGeolocation](https://usehooks.com/usegeolocation) - [useHistoryState](https://usehooks.com/usehistorystate) - [useHover](https://usehooks.com/usehover) - [useIdle](https://usehooks.com/useidle) - [useIntersectionObserver](https://usehooks.com/useintersectionobserver) - [useIsClient](https://usehooks.com/useisclient) - [useIsFirstRender](https://usehooks.com/useisfirstrender) - [useList](https://usehooks.com/uselist) - [useLocalStorage](https://usehooks.com/uselocalstorage) - [useLockBodyScroll](https://usehooks.com/uselockbodyscroll) - [useLongPress](https://usehooks.com/uselongpress) - [useMap](https://usehooks.com/usemap) - [useMeasure](https://usehooks.com/usemeasure) - [useMediaQuery](https://usehooks.com/usemediaquery) - [useMouse](https://usehooks.com/usemouse) - [useNetworkState](https://usehooks.com/usenetworkstate) - [useObjectState](https://usehooks.com/useobjectstate) - [useOrientation](https://usehooks.com/useorientation) - [usePreferredLanguage](https://usehooks.com/usepreferredlanguage) - [usePrevious](https://usehooks.com/useprevious) - [useQueue](https://usehooks.com/usequeue) - [useRenderCount](https://usehooks.com/userendercount) - [useRenderInfo](https://usehooks.com/userenderinfo) - [useScript](https://usehooks.com/usescript) - [useSessionStorage](https://usehooks.com/usesessionstorage) - [useSet](https://usehooks.com/useset) - [useThrottle](https://usehooks.com/usethrottle) - [useToggle](https://usehooks.com/usetoggle) - [useVisibilityChange](https://usehooks.com/usevisibilitychange) - [useWindowScroll](https://usehooks.com/usewindowscroll) - [useWindowSize](https://usehooks.com/usewindowsize) ## Experimental ### Install `npm i @uidotdev/usehooks@experimental react@experimental react-dom@experimental` ### Hooks - [useContinuousRetry](https://usehooks.com/usecontinuousretry) - [useCountdown](https://usehooks.com/usecountdown) - [useEventListener](https://usehooks.com/useeventlistener) - [useFetch](https://usehooks.com/usefetch) - [useInterval](https://usehooks.com/useinterval) - [useIntervalWhen](https://usehooks.com/useintervalwhen) - [useKeyPress](https://usehooks.com/usekeypress) - [useLogger](https://usehooks.com/uselogger) - [usePageLeave](https://usehooks.com/usepageleave) - [useRandomInterval](https://usehooks.com/userandominterval) - [useTimeout](https://usehooks.com/usetimeout) ================================================ FILE: index.d.ts ================================================ import * as React from "react"; export type BatteryManager = { supported: boolean; loading: boolean; level: number | null; charging: boolean | null; chargingTime: number | null; dischargingTime: number | null; }; export type GeolocationState = { loading: boolean; accuracy: number | null; altitude: number | null; altitudeAccuracy: number | null; heading: number | null; latitude: number | null; longitude: number | null; speed: number | null; timestamp: number | null; error: GeolocationPositionError | null; }; export type HistoryState = { state: T; set: (newPresent: T) => void; undo: () => void; redo: () => void; clear: () => void; canUndo: boolean; canRedo: boolean; }; export type LongPressOptions = { threshold?: number; onStart?: (e: Event) => void; onFinish?: (e: Event) => void; onCancel?: (e: Event) => void; }; export type LongPressFns = { onMouseDown: (e: React.MouseEvent) => void; onMouseUp: (e: React.MouseEvent) => void; onMouseLeave: (e: React.MouseEvent) => void; onTouchStart: (e: React.TouchEvent) => void; onTouchEnd: (e: React.TouchEvent) => void; }; export type MousePosition = { x: number; y: number; elementX: number; elementY: number; elementPositionX: number; elementPositionY: number; }; export type NetworkState = { online: boolean; downlink: number | null; downlinkMax: number | null; effectiveType: string | null; rtt: number | null; saveData: boolean | null; type: string | null; }; export type CustomList = { set: (l: T[]) => void; push: (element: T) => void; removeAt: (index: number) => void; insertAt: (index: number, element: T) => void; updateAt: (index: number, element: T) => void; clear: () => void; }; export type CustomQueue = { add: (element: T) => void; remove: () => T | undefined; clear: () => void; first: T | undefined; last: T | undefined; size: number; queue: T[]; }; export type RenderInfo = { name: string; renders: number; sinceLastRender: number; timestamp: number; }; export type SpeechOptions = { lang?: string; voice?: { lang?: string; name?: string; }; rate?: number; pitch?: number; volume?: number; }; export type SpeechState = { isPlaying: boolean; status: "init" | "play" | "pause" | "stop"; lang: string; voiceInfo: { lang: string; name: string; }; rate: number; pitch: number; volume: number; }; declare module "@uidotdev/usehooks" { export function useBattery(): BatteryManager; export function useClickAway( cb: (e: Event) => void ): React.MutableRefObject; export function useCopyToClipboard(): [ string | null, (value: string) => Promise ]; export function useCounter( startingValue?: number, options?: { min?: number; max?: number; } ): [ number, { increment: () => void; decrement: () => void; set: (nextCount: number) => void; reset: () => void; } ]; export function useDebounce(value: T, delay: number): T; export function useDefault( initialValue: T, defaultValue: T ): [T, React.Dispatch>]; export function useDocumentTitle(title: string): void; export function useFavicon(url: string): void; export function useGeolocation(options?: PositionOptions): GeolocationState; export function useHistoryState(initialPresent?: T): HistoryState; export function useHover(): [ React.RefCallback, boolean ]; export function useIdle(ms?: number): boolean; export function useIntersectionObserver( options?: IntersectionObserverInit ): [React.RefCallback, IntersectionObserverEntry | null]; export function useIsClient(): boolean; export function useIsFirstRender(): boolean; export function useList(defaultList?: T[]): [T[], CustomList]; export function useLocalStorage( key: string, initialValue?: T ): [T, React.Dispatch>]; export function useLockBodyScroll(): void; export function useLongPress( callback: (e: Event) => void, options?: LongPressOptions ): LongPressFns; export function useMap(initialState?: T): Map; export function useMeasure(): [ React.RefCallback, { width: number | null; height: number | null; } ]; export function useMediaQuery(query: string): boolean; export function useMouse(): [ MousePosition, React.MutableRefObject ]; export function useNetworkState(): NetworkState; export function useObjectState(initialValue: T): [T, (arg: T) => void]; export function useOrientation(): { angle: number; type: string; }; export function usePreferredLanguage(): string; export function usePrevious(newValue: T): T; export function useQueue(initialValue?: T[]): CustomQueue; export function useRenderCount(): number; export function useRenderInfo(name?: string): RenderInfo | undefined; export function useScript( src: string, options?: { removeOnUnmount?: boolean; } ): "unknown" | "loading" | "ready" | "error"; export function useSessionStorage( key: string, initialValue: T ): [T, React.Dispatch>]; export function useSet(values?: T[]): Set; export function useSpeech(text: string, options?: SpeechOptions): SpeechState; export function useThrottle(value: T, delay: number): T; export function useToggle( initialValue?: boolean ): [boolean, (newValue?: boolean) => void]; export function useVisibilityChange(): boolean; export function useWindowScroll(): [ { x: number | null; y: number | null; }, (args: unknown) => void ]; export function useWindowSize(): { width: number | null; height: number | null; }; } ================================================ FILE: index.js ================================================ import * as React from "react"; function isShallowEqual(object1, object2) { const keys1 = Object.keys(object1); const keys2 = Object.keys(object2); if (keys1.length !== keys2.length) { return false; } for (let key of keys1) { if (object1[key] !== object2[key]) { return false; } } return true; } function isTouchEvent({ nativeEvent }) { return window.TouchEvent ? nativeEvent instanceof TouchEvent : "touches" in nativeEvent; } function isMouseEvent(event) { return event.nativeEvent instanceof MouseEvent; } function throttle(cb, ms) { let lastTime = 0; return () => { const now = Date.now(); if (now - lastTime >= ms) { cb(); lastTime = now; } }; } function isPlainObject(value) { return Object.prototype.toString.call(value) === "[object Object]"; } function dispatchStorageEvent(key, newValue) { window.dispatchEvent(new StorageEvent("storage", { key, newValue })); } export function useBattery() { const [state, setState] = React.useState({ supported: true, loading: true, level: null, charging: null, chargingTime: null, dischargingTime: null, }); React.useEffect(() => { if (!navigator.getBattery) { setState((s) => ({ ...s, supported: false, loading: false, })); return; } let battery = null; const handleChange = () => { setState({ supported: true, loading: false, level: battery.level, charging: battery.charging, chargingTime: battery.chargingTime, dischargingTime: battery.dischargingTime, }); }; navigator.getBattery().then((b) => { battery = b; handleChange(); b.addEventListener("levelchange", handleChange); b.addEventListener("chargingchange", handleChange); b.addEventListener("chargingtimechange", handleChange); b.addEventListener("dischargingtimechange", handleChange); }); return () => { if (battery) { battery.removeEventListener("levelchange", handleChange); battery.removeEventListener("chargingchange", handleChange); battery.removeEventListener("chargingtimechange", handleChange); battery.removeEventListener("dischargingtimechange", handleChange); } }; }, []); return state; } export function useClickAway(cb) { const ref = React.useRef(null); const refCb = React.useRef(cb); React.useLayoutEffect(() => { refCb.current = cb; }); React.useEffect(() => { const handler = (e) => { const element = ref.current; if (element && !element.contains(e.target)) { refCb.current(e); } }; document.addEventListener("mousedown", handler); document.addEventListener("touchstart", handler); return () => { document.removeEventListener("mousedown", handler); document.removeEventListener("touchstart", handler); }; }, []); return ref; } function oldSchoolCopy(text) { const tempTextArea = document.createElement("textarea"); tempTextArea.value = text; document.body.appendChild(tempTextArea); tempTextArea.select(); document.execCommand("copy"); document.body.removeChild(tempTextArea); } export function useCopyToClipboard() { const [state, setState] = React.useState(null); const copyToClipboard = React.useCallback((value) => { const handleCopy = async () => { try { if (navigator?.clipboard?.writeText) { await navigator.clipboard.writeText(value); setState(value); } else { throw new Error("writeText not supported"); } } catch (e) { oldSchoolCopy(value); setState(value); } }; handleCopy(); }, []); return [state, copyToClipboard]; } export function useCounter(startingValue = 0, options = {}) { const { min, max } = options; if (typeof min === "number" && startingValue < min) { throw new Error( `Your starting value of ${startingValue} is less than your min of ${min}.` ); } if (typeof max === "number" && startingValue > max) { throw new Error( `Your starting value of ${startingValue} is greater than your max of ${max}.` ); } const [count, setCount] = React.useState(startingValue); const increment = React.useCallback(() => { setCount((c) => { const nextCount = c + 1; if (typeof max === "number" && nextCount > max) { return c; } return nextCount; }); }, [max]); const decrement = React.useCallback(() => { setCount((c) => { const nextCount = c - 1; if (typeof min === "number" && nextCount < min) { return c; } return nextCount; }); }, [min]); const set = React.useCallback( (nextCount) => { setCount((c) => { if (typeof max === "number" && nextCount > max) { return c; } if (typeof min === "number" && nextCount < min) { return c; } return nextCount; }); }, [max, min] ); const reset = React.useCallback(() => { setCount(startingValue); }, [startingValue]); return [ count, { increment, decrement, set, reset, }, ]; } export function useDebounce(value, delay) { const [debouncedValue, setDebouncedValue] = React.useState(value); React.useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(value); }, delay); return () => { clearTimeout(handler); }; }, [value, delay]); return debouncedValue; } export function useDefault(initialValue, defaultValue) { const [state, setState] = React.useState(initialValue); if (typeof state === "undefined" || state === null) { return [defaultValue, setState]; } return [state, setState]; } export function useDocumentTitle(title) { React.useEffect(() => { document.title = title; }, [title]); } export function useFavicon(url) { React.useEffect(() => { let link = document.querySelector(`link[rel~="icon"]`); if (!link) { link = document.createElement("link"); link.type = "image/x-icon"; link.rel = "icon"; link.href = url; document.head.appendChild(link); } else { link.href = url; } }, [url]); } export function useGeolocation(options = {}) { const [state, setState] = React.useState({ loading: true, accuracy: null, altitude: null, altitudeAccuracy: null, heading: null, latitude: null, longitude: null, speed: null, timestamp: null, error: null, }); const optionsRef = React.useRef(options); React.useEffect(() => { const onEvent = ({ coords, timestamp }) => { setState({ loading: false, timestamp, latitude: coords.latitude, longitude: coords.longitude, altitude: coords.altitude, accuracy: coords.accuracy, altitudeAccuracy: coords.altitudeAccuracy, heading: coords.heading, speed: coords.speed, }); }; const onEventError = (error) => { setState((s) => ({ ...s, loading: false, error, })); }; navigator.geolocation.getCurrentPosition( onEvent, onEventError, optionsRef.current ); const watchId = navigator.geolocation.watchPosition( onEvent, onEventError, optionsRef.current ); return () => { navigator.geolocation.clearWatch(watchId); }; }, []); return state; } const initialUseHistoryStateState = { past: [], present: null, future: [], }; const useHistoryStateReducer = (state, action) => { const { past, present, future } = state; if (action.type === "UNDO") { return { past: past.slice(0, past.length - 1), present: past[past.length - 1], future: [present, ...future], }; } else if (action.type === "REDO") { return { past: [...past, present], present: future[0], future: future.slice(1), }; } else if (action.type === "SET") { const { newPresent } = action; if (action.newPresent === present) { return state; } return { past: [...past, present], present: newPresent, future: [], }; } else if (action.type === "CLEAR") { return { ...initialUseHistoryStateState, present: action.initialPresent, }; } else { throw new Error("Unsupported action type"); } }; export function useHistoryState(initialPresent = {}) { const initialPresentRef = React.useRef(initialPresent); const [state, dispatch] = React.useReducer(useHistoryStateReducer, { ...initialUseHistoryStateState, present: initialPresentRef.current, }); const canUndo = state.past.length !== 0; const canRedo = state.future.length !== 0; const undo = React.useCallback(() => { if (canUndo) { dispatch({ type: "UNDO" }); } }, [canUndo]); const redo = React.useCallback(() => { if (canRedo) { dispatch({ type: "REDO" }); } }, [canRedo]); const set = React.useCallback( (newPresent) => dispatch({ type: "SET", newPresent }), [] ); const clear = React.useCallback( () => dispatch({ type: "CLEAR", initialPresent: initialPresentRef.current }), [] ); return { state: state.present, set, undo, redo, clear, canUndo, canRedo }; } export function useHover() { const [hovering, setHovering] = React.useState(false); const previousNode = React.useRef(null); const handleMouseEnter = React.useCallback(() => { setHovering(true); }, []); const handleMouseLeave = React.useCallback(() => { setHovering(false); }, []); const customRef = React.useCallback( (node) => { if (previousNode.current?.nodeType === Node.ELEMENT_NODE) { previousNode.current.removeEventListener( "mouseenter", handleMouseEnter ); previousNode.current.removeEventListener( "mouseleave", handleMouseLeave ); } if (node?.nodeType === Node.ELEMENT_NODE) { node.addEventListener("mouseenter", handleMouseEnter); node.addEventListener("mouseleave", handleMouseLeave); } previousNode.current = node; }, [handleMouseEnter, handleMouseLeave] ); return [customRef, hovering]; } export function useIdle(ms = 1000 * 60) { const [idle, setIdle] = React.useState(false); React.useEffect(() => { let timeoutId; const handleTimeout = () => { setIdle(true); }; const handleEvent = throttle((e) => { setIdle(false); window.clearTimeout(timeoutId); timeoutId = window.setTimeout(handleTimeout, ms); }, 500); const handleVisibilityChange = () => { if (!document.hidden) { handleEvent(); } }; timeoutId = window.setTimeout(handleTimeout, ms); window.addEventListener("mousemove", handleEvent); window.addEventListener("mousedown", handleEvent); window.addEventListener("resize", handleEvent); window.addEventListener("keydown", handleEvent); window.addEventListener("touchstart", handleEvent); window.addEventListener("wheel", handleEvent); document.addEventListener("visibilitychange", handleVisibilityChange); return () => { window.removeEventListener("mousemove", handleEvent); window.removeEventListener("mousedown", handleEvent); window.removeEventListener("resize", handleEvent); window.removeEventListener("keydown", handleEvent); window.removeEventListener("touchstart", handleEvent); window.removeEventListener("wheel", handleEvent); document.removeEventListener("visibilitychange", handleVisibilityChange); window.clearTimeout(timeoutId); }; }, [ms]); return idle; } export function useIntersectionObserver(options = {}) { const { threshold = 1, root = null, rootMargin = "0px" } = options; const [entry, setEntry] = React.useState(null); const previousObserver = React.useRef(null); const customRef = React.useCallback( (node) => { if (previousObserver.current) { previousObserver.current.disconnect(); previousObserver.current = null; } if (node?.nodeType === Node.ELEMENT_NODE) { const observer = new IntersectionObserver( ([entry]) => { setEntry(entry); }, { threshold, root, rootMargin } ); observer.observe(node); previousObserver.current = observer; } }, [threshold, root, rootMargin] ); return [customRef, entry]; } export function useIsClient() { const [isClient, setIsClient] = React.useState(false); React.useEffect(() => { setIsClient(true); }, []); return isClient; } export function useIsFirstRender() { const renderRef = React.useRef(true); if (renderRef.current === true) { renderRef.current = false; return true; } return renderRef.current; } export function useList(defaultList = []) { const [list, setList] = React.useState(defaultList); const set = React.useCallback((l) => { setList(l); }, []); const push = React.useCallback((element) => { setList((l) => [...l, element]); }, []); const removeAt = React.useCallback((index) => { setList((l) => [...l.slice(0, index), ...l.slice(index + 1)]); }, []); const insertAt = React.useCallback((index, element) => { setList((l) => [...l.slice(0, index), element, ...l.slice(index)]); }, []); const updateAt = React.useCallback((index, element) => { setList((l) => l.map((e, i) => (i === index ? element : e))); }, []); const clear = React.useCallback(() => setList([]), []); return [list, { set, push, removeAt, insertAt, updateAt, clear }]; } const setLocalStorageItem = (key, value) => { const stringifiedValue = JSON.stringify(value); window.localStorage.setItem(key, stringifiedValue); dispatchStorageEvent(key, stringifiedValue); }; const removeLocalStorageItem = (key) => { window.localStorage.removeItem(key); dispatchStorageEvent(key, null); }; const getLocalStorageItem = (key) => { return window.localStorage.getItem(key); }; const useLocalStorageSubscribe = (callback) => { window.addEventListener("storage", callback); return () => window.removeEventListener("storage", callback); }; const getLocalStorageServerSnapshot = () => { throw Error("useLocalStorage is a client-only hook"); }; export function useLocalStorage(key, initialValue) { const getSnapshot = () => getLocalStorageItem(key); const store = React.useSyncExternalStore( useLocalStorageSubscribe, getSnapshot, getLocalStorageServerSnapshot ); const setState = React.useCallback( (v) => { try { const nextState = typeof v === "function" ? v(JSON.parse(store)) : v; if (nextState === undefined || nextState === null) { removeLocalStorageItem(key); } else { setLocalStorageItem(key, nextState); } } catch (e) { console.warn(e); } }, [key, store] ); React.useEffect(() => { if ( getLocalStorageItem(key) === null && typeof initialValue !== "undefined" ) { setLocalStorageItem(key, initialValue); } }, [key, initialValue]); return [store ? JSON.parse(store) : initialValue, setState]; } export function useLockBodyScroll() { React.useLayoutEffect(() => { const originalStyle = window.getComputedStyle(document.body).overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = originalStyle; }; }, []); } export function useLongPress(callback, options = {}) { const { threshold = 400, onStart, onFinish, onCancel } = options; const isLongPressActive = React.useRef(false); const isPressed = React.useRef(false); const timerId = React.useRef(); return React.useMemo(() => { if (typeof callback !== "function") { return {}; } const start = (event) => { if (!isMouseEvent(event) && !isTouchEvent(event)) return; if (onStart) { onStart(event); } isPressed.current = true; timerId.current = setTimeout(() => { callback(event); isLongPressActive.current = true; }, threshold); }; const cancel = (event) => { if (!isMouseEvent(event) && !isTouchEvent(event)) return; if (isLongPressActive.current) { if (onFinish) { onFinish(event); } } else if (isPressed.current) { if (onCancel) { onCancel(event); } } isLongPressActive.current = false; isPressed.current = false; if (timerId.current) { window.clearTimeout(timerId.current); } }; const mouseHandlers = { onMouseDown: start, onMouseUp: cancel, onMouseLeave: cancel, }; const touchHandlers = { onTouchStart: start, onTouchEnd: cancel, }; return { ...mouseHandlers, ...touchHandlers, }; }, [callback, threshold, onCancel, onFinish, onStart]); } export function useMap(initialState) { const mapRef = React.useRef(new Map(initialState)); const [, reRender] = React.useReducer((x) => x + 1, 0); mapRef.current.set = (...args) => { Map.prototype.set.apply(mapRef.current, args); reRender(); return mapRef.current; }; mapRef.current.clear = (...args) => { Map.prototype.clear.apply(mapRef.current, args); reRender(); }; mapRef.current.delete = (...args) => { const res = Map.prototype.delete.apply(mapRef.current, args); reRender(); return res; }; return mapRef.current; } export function useMeasure() { const [dimensions, setDimensions] = React.useState({ width: null, height: null, }); const previousObserver = React.useRef(null); const customRef = React.useCallback((node) => { if (previousObserver.current) { previousObserver.current.disconnect(); previousObserver.current = null; } if (node?.nodeType === Node.ELEMENT_NODE) { const observer = new ResizeObserver(([entry]) => { if (entry && entry.borderBoxSize) { const { inlineSize: width, blockSize: height } = entry.borderBoxSize[0]; setDimensions({ width, height }); } }); observer.observe(node); previousObserver.current = observer; } }, []); return [customRef, dimensions]; } export function useMediaQuery(query) { const subscribe = React.useCallback( (callback) => { const matchMedia = window.matchMedia(query); matchMedia.addEventListener("change", callback); return () => { matchMedia.removeEventListener("change", callback); }; }, [query] ); const getSnapshot = () => { return window.matchMedia(query).matches; }; const getServerSnapshot = () => { throw Error("useMediaQuery is a client-only hook"); }; return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); } export function useMouse() { const [state, setState] = React.useState({ x: 0, y: 0, elementX: 0, elementY: 0, elementPositionX: 0, elementPositionY: 0, }); const ref = React.useRef(null); React.useLayoutEffect(() => { const handleMouseMove = (event) => { let newState = { x: event.pageX, y: event.pageY, }; if (ref.current?.nodeType === Node.ELEMENT_NODE) { const { left, top } = ref.current.getBoundingClientRect(); const elementPositionX = left + window.scrollX; const elementPositionY = top + window.scrollY; const elementX = event.pageX - elementPositionX; const elementY = event.pageY - elementPositionY; newState.elementX = elementX; newState.elementY = elementY; newState.elementPositionX = elementPositionX; newState.elementPositionY = elementPositionY; } setState((s) => { return { ...s, ...newState, }; }); }; document.addEventListener("mousemove", handleMouseMove); return () => { document.removeEventListener("mousemove", handleMouseMove); }; }, []); return [state, ref]; } const getConnection = () => { return ( navigator?.connection || navigator?.mozConnection || navigator?.webkitConnection ); }; const useNetworkStateSubscribe = (callback) => { window.addEventListener("online", callback, { passive: true }); window.addEventListener("offline", callback, { passive: true }); const connection = getConnection(); if (connection) { connection.addEventListener("change", callback, { passive: true }); } return () => { window.removeEventListener("online", callback); window.removeEventListener("offline", callback); if (connection) { connection.removeEventListener("change", callback); } }; }; const getNetworkStateServerSnapshot = () => { throw Error("useNetworkState is a client-only hook"); }; export function useNetworkState() { const cache = React.useRef({}); const getSnapshot = () => { const online = navigator.onLine; const connection = getConnection(); const nextState = { online, downlink: connection?.downlink, downlinkMax: connection?.downlinkMax, effectiveType: connection?.effectiveType, rtt: connection?.rtt, saveData: connection?.saveData, type: connection?.type, }; if (isShallowEqual(cache.current, nextState)) { return cache.current; } else { cache.current = nextState; return nextState; } }; return React.useSyncExternalStore( useNetworkStateSubscribe, getSnapshot, getNetworkStateServerSnapshot ); } export function useObjectState(initialValue) { const [state, setState] = React.useState(initialValue); const handleUpdate = React.useCallback((arg) => { if (typeof arg === "function") { setState((s) => { const newState = arg(s); if (isPlainObject(newState)) { return { ...s, ...newState, }; } }); } if (isPlainObject(arg)) { setState((s) => ({ ...s, ...arg, })); } }, []); return [state, handleUpdate]; } export function useOrientation() { const [orientation, setOrientation] = React.useState({ angle: 0, type: "landscape-primary", }); React.useLayoutEffect(() => { const handleChange = () => { const { angle, type } = window.screen.orientation; setOrientation({ angle, type, }); }; const handle_orientationchange = () => { setOrientation({ type: "UNKNOWN", angle: window.orientation, }); }; if (window.screen?.orientation) { handleChange(); window.screen.orientation.addEventListener("change", handleChange); } else { handle_orientationchange(); window.addEventListener("orientationchange", handle_orientationchange); } return () => { if (window.screen?.orientation) { window.screen.orientation.removeEventListener("change", handleChange); } else { window.removeEventListener( "orientationchange", handle_orientationchange ); } }; }, []); return orientation; } const usePreferredLanguageSubscribe = (cb) => { window.addEventListener("languagechange", cb); return () => window.removeEventListener("languagechange", cb); }; const getPreferredLanguageSnapshot = () => { return navigator.language; }; const getPreferredLanguageServerSnapshot = () => { throw Error("usePreferredLanguage is a client-only hook"); }; export function usePreferredLanguage() { return React.useSyncExternalStore( usePreferredLanguageSubscribe, getPreferredLanguageSnapshot, getPreferredLanguageServerSnapshot ); } export function usePrevious(value) { const [current, setCurrent] = React.useState(value); const [previous, setPrevious] = React.useState(null); if (value !== current) { setPrevious(current); setCurrent(value); } return previous; } export function useQueue(initialValue = []) { const [queue, setQueue] = React.useState(initialValue); const add = React.useCallback((element) => { setQueue((q) => [...q, element]); }, []); const remove = React.useCallback(() => { let removedElement; setQueue(([first, ...q]) => { removedElement = first; return q; }); return removedElement; }, []); const clear = React.useCallback(() => { setQueue([]); }, []); return { add, remove, clear, first: queue[0], last: queue[queue.length - 1], size: queue.length, queue, }; } export function useRenderCount() { const count = React.useRef(0); count.current++; return count.current; } export function useRenderInfo(name = "Unknown") { const count = React.useRef(0); const lastRender = React.useRef(); const now = Date.now(); count.current++; React.useEffect(() => { lastRender.current = Date.now(); }); const sinceLastRender = lastRender.current ? now - lastRender.current : 0; if (process.env.NODE_ENV !== "production") { const info = { name, renders: count.current, sinceLastRender, timestamp: now, }; console.log(info); return info; } } export function useScript(src, options = {}) { const [status, setStatus] = React.useState("loading"); const optionsRef = React.useRef(options); React.useEffect(() => { let script = document.querySelector(`script[src="${src}"]`); const domStatus = script?.getAttribute("data-status"); if (domStatus) { setStatus(domStatus); return; } if (script === null) { script = document.createElement("script"); script.src = src; script.async = true; script.setAttribute("data-status", "loading"); document.body.appendChild(script); const handleScriptLoad = () => { script.setAttribute("data-status", "ready"); setStatus("ready"); removeEventListeners(); }; const handleScriptError = () => { script.setAttribute("data-status", "error"); setStatus("error"); removeEventListeners(); }; const removeEventListeners = () => { script.removeEventListener("load", handleScriptLoad); script.removeEventListener("error", handleScriptError); }; script.addEventListener("load", handleScriptLoad); script.addEventListener("error", handleScriptError); const removeOnUnmount = optionsRef.current.removeOnUnmount; return () => { if (removeOnUnmount === true) { script.remove(); removeEventListeners(); } }; } else { setStatus("unknown"); } }, [src]); return status; } const setSessionStorageItem = (key, value) => { const stringifiedValue = JSON.stringify(value); window.sessionStorage.setItem(key, stringifiedValue); dispatchStorageEvent(key, stringifiedValue); }; const removeSessionStorageItem = (key) => { window.sessionStorage.removeItem(key); dispatchStorageEvent(key, null); }; const getSessionStorageItem = (key) => { return window.sessionStorage.getItem(key); }; const useSessionStorageSubscribe = (callback) => { window.addEventListener("storage", callback); return () => window.removeEventListener("storage", callback); }; const getSessionStorageServerSnapshot = () => { throw Error("useSessionStorage is a client-only hook"); }; export function useSessionStorage(key, initialValue) { const getSnapshot = () => getSessionStorageItem(key); const store = React.useSyncExternalStore( useSessionStorageSubscribe, getSnapshot, getSessionStorageServerSnapshot ); const setState = React.useCallback( (v) => { try { const nextState = typeof v === "function" ? v(JSON.parse(store)) : v; if (nextState === undefined || nextState === null) { removeSessionStorageItem(key); } else { setSessionStorageItem(key, nextState); } } catch (e) { console.warn(e); } }, [key, store] ); React.useEffect(() => { if ( getSessionStorageItem(key) === null && typeof initialValue !== "undefined" ) { setSessionStorageItem(key, initialValue); } }, [key, initialValue]); return [store ? JSON.parse(store) : initialValue, setState]; } export function useSet(values) { const setRef = React.useRef(new Set(values)); const [, reRender] = React.useReducer((x) => x + 1, 0); setRef.current.add = (...args) => { const res = Set.prototype.add.apply(setRef.current, args); reRender(); return res; }; setRef.current.clear = (...args) => { Set.prototype.clear.apply(setRef.current, args); reRender(); }; setRef.current.delete = (...args) => { const res = Set.prototype.delete.apply(setRef.current, args); reRender(); return res; }; return setRef.current; } export function useThrottle(value, interval = 500) { const [throttledValue, setThrottledValue] = React.useState(value); const lastUpdated = React.useRef(null); React.useEffect(() => { const now = Date.now(); if (lastUpdated.current && now >= lastUpdated.current + interval) { lastUpdated.current = now; setThrottledValue(value); } else { const id = window.setTimeout(() => { lastUpdated.current = now; setThrottledValue(value); }, interval); return () => window.clearTimeout(id); } }, [value, interval]); return throttledValue; } export function useToggle(initialValue) { const [on, setOn] = React.useState(() => { if (typeof initialValue === "boolean") { return initialValue; } return Boolean(initialValue); }); const handleToggle = React.useCallback((value) => { if (typeof value === "boolean") { return setOn(value); } return setOn((v) => !v); }, []); return [on, handleToggle]; } const useVisibilityChangeSubscribe = (callback) => { document.addEventListener("visibilitychange", callback); return () => { document.removeEventListener("visibilitychange", callback); }; }; const getVisibilityChangeSnapshot = () => { return document.visibilityState; }; const getVisibilityChangeServerSnapshot = () => { throw Error("useVisibilityChange is a client-only hook"); }; export function useVisibilityChange() { const visibilityState = React.useSyncExternalStore( useVisibilityChangeSubscribe, getVisibilityChangeSnapshot, getVisibilityChangeServerSnapshot ); return visibilityState === "visible"; } export function useWindowScroll() { const [state, setState] = React.useState({ x: null, y: null, }); const scrollTo = React.useCallback((...args) => { if (typeof args[0] === "object") { window.scrollTo(args[0]); } else if (typeof args[0] === "number" && typeof args[1] === "number") { window.scrollTo(args[0], args[1]); } else { throw new Error( `Invalid arguments passed to scrollTo. See here for more info. https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollTo` ); } }, []); React.useLayoutEffect(() => { const handleScroll = () => { setState({ x: window.scrollX, y: window.scrollY }); }; handleScroll(); window.addEventListener("scroll", handleScroll); return () => { window.removeEventListener("scroll", handleScroll); }; }, []); return [state, scrollTo]; } export function useWindowSize() { const [size, setSize] = React.useState({ width: null, height: null, }); React.useLayoutEffect(() => { const handleResize = () => { setSize({ width: window.innerWidth, height: window.innerHeight, }); }; handleResize(); window.addEventListener("resize", handleResize); return () => { window.removeEventListener("resize", handleResize); }; }, []); return size; } ================================================ FILE: package.json ================================================ { "name": "@uidotdev/usehooks", "version": "2.4.1", "description": "A collection of modern, server-safe React hooks – from the ui.dev team", "type": "module", "repository": "uidotdev/usehooks", "devDependencies": { "@types/react": "^18.2.20", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.1.6" }, "exports": { "default": "./index.js" }, "engines": { "node": ">=16" }, "files": [ "index.js", "index.d.ts" ], "types": "index.d.ts", "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" }, "author": "Tyler McGinnis, Ben Adam", "license": "MIT" } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "ESNext", "module": "ESNext", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, "noImplicitAny": true, "allowJs": true, "outDir": "./irrelevant/unused" } } ================================================ FILE: usehooks.com/.astro/types.d.ts ================================================ declare module 'astro:content' { interface Render { '.mdx': Promise<{ Content: import('astro').MarkdownInstance<{}>['Content']; headings: import('astro').MarkdownHeading[]; remarkPluginFrontmatter: Record; }>; } } declare module 'astro:content' { interface Render { '.md': Promise<{ Content: import('astro').MarkdownInstance<{}>['Content']; headings: import('astro').MarkdownHeading[]; remarkPluginFrontmatter: Record; }>; } } declare module 'astro:content' { export { z } from 'astro/zod'; export type CollectionEntry = AnyEntryMap[C][keyof AnyEntryMap[C]]; // TODO: Remove this when having this fallback is no longer relevant. 2.3? 3.0? - erika, 2023-04-04 /** * @deprecated * `astro:content` no longer provide `image()`. * * Please use it through `schema`, like such: * ```ts * import { defineCollection, z } from "astro:content"; * * defineCollection({ * schema: ({ image }) => * z.object({ * image: image(), * }), * }); * ``` */ export const image: never; // This needs to be in sync with ImageMetadata export type ImageFunction = () => import('astro/zod').ZodObject<{ src: import('astro/zod').ZodString; width: import('astro/zod').ZodNumber; height: import('astro/zod').ZodNumber; format: import('astro/zod').ZodUnion< [ import('astro/zod').ZodLiteral<'png'>, import('astro/zod').ZodLiteral<'jpg'>, import('astro/zod').ZodLiteral<'jpeg'>, import('astro/zod').ZodLiteral<'tiff'>, import('astro/zod').ZodLiteral<'webp'>, import('astro/zod').ZodLiteral<'gif'>, import('astro/zod').ZodLiteral<'svg'> ] >; }>; type BaseSchemaWithoutEffects = | import('astro/zod').AnyZodObject | import('astro/zod').ZodUnion | import('astro/zod').ZodDiscriminatedUnion | import('astro/zod').ZodIntersection< import('astro/zod').AnyZodObject, import('astro/zod').AnyZodObject >; type BaseSchema = | BaseSchemaWithoutEffects | import('astro/zod').ZodEffects; export type SchemaContext = { image: ImageFunction }; type DataCollectionConfig = { type: 'data'; schema?: S | ((context: SchemaContext) => S); }; type ContentCollectionConfig = { type?: 'content'; schema?: S | ((context: SchemaContext) => S); }; type CollectionConfig = ContentCollectionConfig | DataCollectionConfig; export function defineCollection( input: CollectionConfig ): CollectionConfig; type AllValuesOf = T extends any ? T[keyof T] : never; type ValidContentEntrySlug = AllValuesOf< ContentEntryMap[C] >['slug']; export function getEntryBySlug< C extends keyof ContentEntryMap, E extends ValidContentEntrySlug | (string & {}) >( collection: C, // Note that this has to accept a regular string too, for SSR entrySlug: E ): E extends ValidContentEntrySlug ? Promise> : Promise | undefined>; export function getDataEntryById( collection: C, entryId: E ): Promise>; export function getCollection>( collection: C, filter?: (entry: CollectionEntry) => entry is E ): Promise; export function getCollection( collection: C, filter?: (entry: CollectionEntry) => unknown ): Promise[]>; export function getEntry< C extends keyof ContentEntryMap, E extends ValidContentEntrySlug | (string & {}) >(entry: { collection: C; slug: E; }): E extends ValidContentEntrySlug ? Promise> : Promise | undefined>; export function getEntry< C extends keyof DataEntryMap, E extends keyof DataEntryMap[C] | (string & {}) >(entry: { collection: C; id: E; }): E extends keyof DataEntryMap[C] ? Promise : Promise | undefined>; export function getEntry< C extends keyof ContentEntryMap, E extends ValidContentEntrySlug | (string & {}) >( collection: C, slug: E ): E extends ValidContentEntrySlug ? Promise> : Promise | undefined>; export function getEntry< C extends keyof DataEntryMap, E extends keyof DataEntryMap[C] | (string & {}) >( collection: C, id: E ): E extends keyof DataEntryMap[C] ? Promise : Promise | undefined>; /** Resolve an array of entry references from the same collection */ export function getEntries( entries: { collection: C; slug: ValidContentEntrySlug; }[] ): Promise[]>; export function getEntries( entries: { collection: C; id: keyof DataEntryMap[C]; }[] ): Promise[]>; export function reference( collection: C ): import('astro/zod').ZodEffects< import('astro/zod').ZodString, C extends keyof ContentEntryMap ? { collection: C; slug: ValidContentEntrySlug; } : { collection: C; id: keyof DataEntryMap[C]; } >; // Allow generic `string` to avoid excessive type errors in the config // if `dev` is not running to update as you edit. // Invalid collection names will be caught at build time. export function reference( collection: C ): import('astro/zod').ZodEffects; type ReturnTypeOrOriginal = T extends (...args: any[]) => infer R ? R : T; type InferEntrySchema = import('astro/zod').infer< ReturnTypeOrOriginal['schema']> >; type ContentEntryMap = { "hooks": { "useBattery.mdx": { id: "useBattery.mdx"; slug: "usebattery"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useClickAway.mdx": { id: "useClickAway.mdx"; slug: "useclickaway"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useContinuousRetry.mdx": { id: "useContinuousRetry.mdx"; slug: "usecontinuousretry"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useCopyToClipboard.mdx": { id: "useCopyToClipboard.mdx"; slug: "usecopytoclipboard"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useCountdown.mdx": { id: "useCountdown.mdx"; slug: "usecountdown"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useCounter.mdx": { id: "useCounter.mdx"; slug: "usecounter"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useDebounce.mdx": { id: "useDebounce.mdx"; slug: "usedebounce"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useDefault.mdx": { id: "useDefault.mdx"; slug: "usedefault"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useDocumentTitle.mdx": { id: "useDocumentTitle.mdx"; slug: "usedocumenttitle"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useEventListener.mdx": { id: "useEventListener.mdx"; slug: "useeventlistener"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useFavicon.mdx": { id: "useFavicon.mdx"; slug: "usefavicon"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useFetch.mdx": { id: "useFetch.mdx"; slug: "usefetch"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useGeolocation.mdx": { id: "useGeolocation.mdx"; slug: "usegeolocation"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useHistoryState.mdx": { id: "useHistoryState.mdx"; slug: "usehistorystate"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useHover.mdx": { id: "useHover.mdx"; slug: "usehover"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useIdle.mdx": { id: "useIdle.mdx"; slug: "useidle"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useIntersectionObserver.mdx": { id: "useIntersectionObserver.mdx"; slug: "useintersectionobserver"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useInterval.mdx": { id: "useInterval.mdx"; slug: "useinterval"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useIntervalWhen.mdx": { id: "useIntervalWhen.mdx"; slug: "useintervalwhen"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useIsClient.mdx": { id: "useIsClient.mdx"; slug: "useisclient"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useIsFirstRender.mdx": { id: "useIsFirstRender.mdx"; slug: "useisfirstrender"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useKeyPress.mdx": { id: "useKeyPress.mdx"; slug: "usekeypress"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useList.mdx": { id: "useList.mdx"; slug: "uselist"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useLocalStorage.mdx": { id: "useLocalStorage.mdx"; slug: "uselocalstorage"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useLockBodyScroll.mdx": { id: "useLockBodyScroll.mdx"; slug: "uselockbodyscroll"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useLogger.mdx": { id: "useLogger.mdx"; slug: "uselogger"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useLongPress.mdx": { id: "useLongPress.mdx"; slug: "uselongpress"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useMap.mdx": { id: "useMap.mdx"; slug: "usemap"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useMeasure.mdx": { id: "useMeasure.mdx"; slug: "usemeasure"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useMediaQuery.mdx": { id: "useMediaQuery.mdx"; slug: "usemediaquery"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useMouse.mdx": { id: "useMouse.mdx"; slug: "usemouse"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useNetworkState.mdx": { id: "useNetworkState.mdx"; slug: "usenetworkstate"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useObjectState.mdx": { id: "useObjectState.mdx"; slug: "useobjectstate"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useOrientation.mdx": { id: "useOrientation.mdx"; slug: "useorientation"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "usePageLeave.mdx": { id: "usePageLeave.mdx"; slug: "usepageleave"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "usePreferredLanguage.mdx": { id: "usePreferredLanguage.mdx"; slug: "usepreferredlanguage"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "usePrevious.mdx": { id: "usePrevious.mdx"; slug: "useprevious"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useQueue.mdx": { id: "useQueue.mdx"; slug: "usequeue"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useRandomInterval.mdx": { id: "useRandomInterval.mdx"; slug: "userandominterval"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useRenderCount.mdx": { id: "useRenderCount.mdx"; slug: "userendercount"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useRenderInfo.mdx": { id: "useRenderInfo.mdx"; slug: "userenderinfo"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useScript.mdx": { id: "useScript.mdx"; slug: "usescript"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useSessionStorage.mdx": { id: "useSessionStorage.mdx"; slug: "usesessionstorage"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useSet.mdx": { id: "useSet.mdx"; slug: "useset"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useThrottle.mdx": { id: "useThrottle.mdx"; slug: "usethrottle"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useTimeout.mdx": { id: "useTimeout.mdx"; slug: "usetimeout"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useToggle.mdx": { id: "useToggle.mdx"; slug: "usetoggle"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useVisibilityChange.mdx": { id: "useVisibilityChange.mdx"; slug: "usevisibilitychange"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useWindowScroll.mdx": { id: "useWindowScroll.mdx"; slug: "usewindowscroll"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; "useWindowSize.mdx": { id: "useWindowSize.mdx"; slug: "usewindowsize"; body: string; collection: "hooks"; data: InferEntrySchema<"hooks"> } & { render(): Render[".mdx"] }; }; }; type DataEntryMap = { }; type AnyEntryMap = ContentEntryMap & DataEntryMap; type ContentConfig = typeof import("../src/content/config"); } ================================================ FILE: usehooks.com/.eslintrc.json ================================================ { "extends": "next/core-web-vitals" } ================================================ FILE: usehooks.com/.gitignore ================================================ node_modules /node_modules .DS_Store .env .env.* .next dist/* /dist public/meta/* !public/meta/og.jpg ================================================ FILE: usehooks.com/.prettierrc ================================================ {} ================================================ FILE: usehooks.com/README.md ================================================ # usehooks.com ## 🚀 Project Structure Inside this project, you'll see the following folders and files: ``` / ├── public/ │ └── favicon.png │ └── img ├── src/ │ ├── components/ │ │ └── Button.astro │ ├── layouts/ │ │ └── Layout.astro │ ├── pages/ │ │ └── index.astro │ ├── sections/ │ │ └── HomeHero.astro └── package.json ``` Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. Any static assets, like images, can be placed in the `public/` directory. ## 🧞 Commands All commands are run from the root of the project, from a terminal: | Command | Action | | :--------------------- | :----------------------------------------------- | | `npm install` | Installs dependencies | | `npm run dev` | Starts local dev server at `localhost:3000` | | `npm run build` | Build your production site to `./dist/` | | `npm run preview` | Preview your build locally, before deploying | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | | `npm run astro --help` | Get help using the Astro CLI | Check out [Astro documentation](https://docs.astro.build). ================================================ FILE: usehooks.com/astro.config.mjs ================================================ import { defineConfig } from "astro/config"; import react from "@astrojs/react"; import customTheme from "./theme.json"; import tailwind from "@astrojs/tailwind"; // import vercel from "@astrojs/vercel/edge"; import mdx from "@astrojs/mdx"; // https://astro.build/config import sitemap from "@astrojs/sitemap"; // https://astro.build/config export default defineConfig({ site: "https://usehooks.com", trailingSlash: "never", integrations: [react(), tailwind(), mdx(), sitemap()], output: "static", // adapter: vercel(), markdown: { shikiConfig: { // Choose from Shiki's built-in themes (or add your own) // https://github.com/shikijs/shiki/blob/main/docs/themes.md theme: customTheme, // Add custom languages // Note: Shiki has countless langs built-in, including .astro! // https://github.com/shikijs/shiki/blob/main/docs/languages.md langs: [], // Enable word wrap to prevent horizontal scrolling wrap: false } } }); ================================================ FILE: usehooks.com/generate-og-images.mjs ================================================ import { readdir, readFile, writeFile } from "fs/promises"; import { join, extname } from "path"; import matter from "gray-matter"; import { Resvg } from "@resvg/resvg-js"; import { html } from "satori-html"; import satori from "satori"; const dir = "./src/content/hooks"; try { const files = await readdir(dir); const logo = await readFile( new URL("./public/img/logo-useHooks.svg", import.meta.url), { encoding: "base64", width: 300, height: 44, } ); for (const file of files) { if (extname(file) !== ".mdx") { continue; } const content = await readFile(join(dir, file), "utf8"); const result = matter(content); await generateOgImage({ title: result.data.name, slug: result.data.name.toLowerCase(), description: result.data.tagline, logo }); } } catch (err) { console.error("Error:", err); } async function generateOgImage({ title, slug, description, logo }) { const svg = await satori( html`
> npm i @uidotdev/usehooks
${title}
${description}
`, { fonts: [ { name: "Outfit", data: await readFile( new URL("./public/fonts/outfit-regular.ttf", import.meta.url) ), weight: "400", style: "normal", }, { name: "Outfit Bold", data: await readFile( new URL("./public/fonts/outfit-bold.ttf", import.meta.url) ), weight: "700", style: "normal", }, { name: "Fira Code", data: await readFile( new URL("./public/fonts/firacode-regular.ttf", import.meta.url) ), weight: "400", style: "normal", }, ], width: 1200, height: 630, } ); const resvg = new Resvg(svg, { fitTo: { mode: "original", }, }); const pngData = resvg.render(); const pngBuffer = pngData.asPng(); await writeFile( new URL(`./public/meta/${slug}.png`, import.meta.url), pngBuffer ); console.log("✅", slug); } ================================================ FILE: usehooks.com/package.json ================================================ { "name": "reacthooks.com", "type": "module", "version": "2.0.0", "private": true, "scripts": { "dev": "astro dev", "start": "astro dev", "build": "node ./generate-og-images.mjs && astro build", "preview": "astro preview", "astro": "astro" }, "dependencies": { "@astrojs/mdx": "^0.19.3", "@astrojs/react": "^2.2.0", "@astrojs/sitemap": "^1.3.1", "@astrojs/tailwind": "^3.1.3", "@astrojs/ts-plugin": "^1.0.6", "@codesandbox/sandpack-react": "^2.6.6", "@types/react": "^18.2.7", "@types/react-dom": "^18.2.4", "astro": "^2.5.2", "classnames": "^2.3.2", "framer-motion": "^10.9.1", "react": "^18.2.0", "react-dom": "^18.2.0", "tailwindcss": "^3.2.7" }, "devDependencies": { "@resvg/resvg-js": "^2.4.1", "satori": "^0.9.1", "satori-html": "^0.3.2" } } ================================================ FILE: usehooks.com/src/components/Button.astro ================================================ --- export interface Props { text: string; size: string; href?: string; type: "link" | "button" | "submit" | "reset"; color: string; class?: string; } const { type, href, text, size, color } = Astro.props; const { class: className } = Astro.props; --- { type === "link" ? {text} : } ================================================ FILE: usehooks.com/src/components/CodePreview.astro ================================================ --- import CodePreview from "./codepreview/CodePreview"; import { getFiles } from "./codepreview/utils"; const { sandboxId, previewHeight } = Astro.props; const files = await getFiles({ id: sandboxId }); ---

Demo:

================================================ FILE: usehooks.com/src/components/CountdownTimer.tsx ================================================ import { Fragment, useEffect, useState } from "react"; interface CountdownProps { targetDate: string; // YYYY-MM-DD format } interface TimeLeft { days: number; hours: number; minutes: number; seconds: number; } function calculateTimeLeft(targetDate: string): TimeLeft { const target = new Date(`${targetDate}T00:00:00-08:00`); const now = new Date(); const difference = +target - +now; if (difference <= 0) { return { days: 0, hours: 0, minutes: 0, seconds: 0, }; } return { days: Math.floor(difference / (1000 * 60 * 60 * 24)), hours: Math.floor((difference / (1000 * 60 * 60)) % 24), minutes: Math.floor((difference / 1000 / 60) % 60), seconds: Math.floor((difference / 1000) % 60), }; } const formatNumber = (number: number) => number.toString().padStart(2, "0"); const Countdown: React.FC = ({ targetDate }) => { const [timeLeft, setTimeLeft] = useState( calculateTimeLeft(targetDate) ); useEffect(() => { const timer = setInterval(() => { const newTimeLeft = calculateTimeLeft(targetDate); setTimeLeft(newTimeLeft); if ( newTimeLeft.days === 0 && newTimeLeft.hours === 0 && newTimeLeft.minutes === 0 && newTimeLeft.seconds === 0 ) { clearInterval(timer); } }, 1000); return () => clearInterval(timer); }, [targetDate]); if ( timeLeft.days === 0 && timeLeft.hours === 0 && timeLeft.minutes === 0 && timeLeft.seconds === 0 ) { return null; } return (
{["days", "hours", "minutes", "seconds"].map((unit, index) => ( {index > 0 && :}
{formatNumber(timeLeft[unit as keyof TimeLeft]).charAt(0)} {formatNumber(timeLeft[unit as keyof TimeLeft]).charAt(1)}

{unit}

))}
); }; export default Countdown; ================================================ FILE: usehooks.com/src/components/HookDescription.astro ================================================ --- import Button from "./Button.astro"; const { name } = Astro.props; ---

Description:

================================================ FILE: usehooks.com/src/components/Install.astro ================================================ --- const { text } = Astro.props; const { class: className } = Astro.props; ---
{text}
================================================ FILE: usehooks.com/src/components/Logo.astro ================================================ --- const { class: className } = Astro.props; --- ================================================ FILE: usehooks.com/src/components/LogoGithub.astro ================================================ --- const { class: className } = Astro.props; --- ================================================ FILE: usehooks.com/src/components/QueryGGBanner.astro ================================================ --- import Button from "./Button.astro"; import CountdownTimer from "./CountdownTimer"; --- ================================================ FILE: usehooks.com/src/components/RelatedHook.astro ================================================ --- import { getEntry } from "astro:content"; import HookCard from "./search/HookCard"; const { slug } = Astro.props; const relatedHook = await getEntry("hooks", slug); const { name, tagline } = relatedHook.data; --- ================================================ FILE: usehooks.com/src/components/StaticCodeContainer.astro ================================================ --- import CodeWrapper from './codepreview/CodeWrapper' ---

Example:

================================================ FILE: usehooks.com/src/components/Svg.astro ================================================ --- export interface Props { name: string; } const { name } = Astro.props as Props; const { default: innerHTML } = await import(`../svg/${name}.svg?raw`); --- ================================================ FILE: usehooks.com/src/components/codepreview/CodePreview.tsx ================================================ import { SandpackProvider, SandpackPreview, SandpackStack, SandpackFiles, } from "@codesandbox/sandpack-react"; import cx from "classnames"; export type PreviewProps = { previewHeight?: string; files?: SandpackFiles | undefined; }; export default function CodePreview({ previewHeight = "250px", files, }: PreviewProps) { const sandpackProviderProps = { files, initMode: "user-visible", autorun: false, logLevel: 0, }; return (
); } ================================================ FILE: usehooks.com/src/components/codepreview/CodeWrapper.tsx ================================================ import { useState } from "react"; import { motion } from "framer-motion"; import { set } from "astro/zod"; const springConfig = { damping: 20, stiffness: 120, mass: 0.15, }; export default function CodeWrapper({ children, }: { children: React.ReactNode; }) { const [expanded, setExpanded] = useState(false); const [hideExpand, setHideExpand] = useState(false); return (
{ if (el) { const { height } = el.getBoundingClientRect(); if (height < 500) { setExpanded(true); setHideExpand(true); } } }} > {children}
{!hideExpand && (
)}
); } ================================================ FILE: usehooks.com/src/components/codepreview/utils.ts ================================================ import { SandpackFile } from "@codesandbox/sandpack-react"; export async function getFiles({ id }: { id: string }) { const configUrl = `https://codesandbox.io/api/v1/sandboxes/${id}/sandpack`; const response = await fetch(configUrl); if (response.ok) { const data = await response.json(); // This hardcodes the active file, we should find a better way to do this return updateFiles(data.files); } return {}; } // https://codesandbox.io/s/challenge-ui-test-zurc8s function getChallengeConfig(json: string) { const csb = JSON.parse(json); if (csb?.previewConfig) { return csb.previewConfig; } return { visibleFiles: [], activeFile: "/src/App.js", }; } export function updateFiles(files: { [key: string]: SandpackFile }) { const previewConfig = getChallengeConfig(files["/package.json"].code); Object.keys(files).map((key) => { if (key === previewConfig.activeFile) { files[key].active = true; } if (!previewConfig.visibleFiles.includes(key)) { files[key].hidden = true; } }); return files; } ================================================ FILE: usehooks.com/src/components/search/Callout.module.css ================================================ .callout :global(a) { height: 100%; padding: var(--body-padding); display: flex; flex-direction: column; align-items: center; gap: 1rem; border: var(--border-dark); border-radius: 0.5rem; font-size: clamp(0.9rem, 2vw, 1.1rem); text-align: center; transition: all 200ms ease-in-out; } .callout :global(a:hover) { transform: scale(1.03); } .callout :global(img:not(.logo)) { max-width: 120px; margin-top: calc(var(--body-padding) * -1.2); margin-bottom: 0.5rem; } .callout :global(img.d20) { width: 70px; } .callout :global(img.money) { width: 110px; } .callout :global(img.spinner) { width: 80px; } .callout :global(img.hot-sauce) { width: 80px; } .callout :global(img.logo) { max-width: 150px; } ================================================ FILE: usehooks.com/src/components/search/Callout.tsx ================================================ import styles from "./Callout.module.css"; type Props = { image: string; imageWidth: string; imageHeight: string; imageAlt: string; pitch: string; }; export default function Callout({ image, imageWidth, imageHeight, imageAlt, pitch, }) { return (
  • {imageAlt} React.gg

    {pitch}

  • ); } ================================================ FILE: usehooks.com/src/components/search/HookCard.module.css ================================================ .hook > :global(a) { height: 100%; padding: var(--body-padding); display: flex; flex-direction: column; gap: 0.6rem; background-color: var(--charcoal); border-radius: 0.5rem; transition: transform 200ms ease-in-out; } .hook > :global(a:hover) { transform: scale(1.03); } .card-title { color: var(--blue); text-transform: none; font-family: var(--font-outfit); font-size: clamp(1.1rem, 3vw, 1.4rem); font-weight: 600; } .card-description { margin-bottom: 0.5rem; font-size: clamp(0.9rem, 2vw, 1.1rem); } .arrow { width: 28px; aspect-ratio: 3 / 2; margin-top: auto; align-self: flex-end; transition: all 200ms ease-in-out; } :global(a:hover) .arrow { transform: translateX(0.6rem); } ================================================ FILE: usehooks.com/src/components/search/HookCard.tsx ================================================ import styles from './HookCard.module.css'; export default function HookCard({ name, tagline, }: { name: string; tagline: string; }) { return (
  • {name}

    {tagline}

  • ); } ================================================ FILE: usehooks.com/src/components/search/HookSearch.module.css ================================================ .hooks-search { position: relative; align-self: flex-start; } .input { padding: 0.4rem 1rem; padding-right: 2rem; background-color: var(--coal); border: var(--border-light); border-radius: 1.5rem; font-size: clamp(0.8rem, 2vw, 1rem); color: var(--white); transition: all 150ms ease-in-out; } .input:focus { outline: none; border-color: var(--charcoal); box-shadow: var(--focus-object); } .input + :global(button) { position: absolute; right: 0.8rem; top: 50%; transform: translateY(-50%); font-size: 1rem; transition: all 150ms ease-in-out; } :global(input[type="search"]::-webkit-search-decoration), :global(input[type="search"]::-webkit-search-cancel-button), :global(input[type="search"]::-webkit-search-results-button), :global(input[type="search"]::-webkit-search-results-decoration) { display: none; } ================================================ FILE: usehooks.com/src/components/search/HookSearch.tsx ================================================ import styles from "./HookSearch.module.css"; export default function HookSearch({ handleChange, handleClear, value, }: { handleClear: () => void; handleChange: (e: React.ChangeEvent) => void; value: string; }) { return (
    ); } ================================================ FILE: usehooks.com/src/components/search/HookSort.module.css ================================================ .toggle { padding: 0.2rem 0.4rem; border: var(--border-light); border-radius: 0.3rem; font-size: clamp(0.7rem, 2vw, 0.9rem); font-weight: 500; transition: all 200ms ease-in-out; } .toggle.active { background-color: var(--yellow); border-color: var(--yellow); color: var(--charcoal); cursor: default; } .toggle:not(.active):hover { background-color: var(--charcoal); } ================================================ FILE: usehooks.com/src/components/search/HookSort.tsx ================================================ import styles from "./HookSort.module.css"; export default function HookSort({ setSort, value, }: { setSort: (value: "name" | "popular") => void; value: "name" | "popular"; }) { return (
    Sort:
    ); } ================================================ FILE: usehooks.com/src/components/search/HooksList.module.css ================================================ .hooks-grid { max-width: 980px; margin: 2rem auto; } .hooks-controls { padding: 1rem var(--body-padding); display: flex; justify-content: flex-end; } .hooks-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); align-items: stretch; gap: 1.6rem; } ================================================ FILE: usehooks.com/src/components/search/HooksList.tsx ================================================ import { useState } from "react"; import Callout from "./Callout"; import HookCard from "./HookCard"; import HookSort from "./HookSort"; import styles from "./HooksList.module.css"; function insertAtIntervals(arr, items) { let newArr = [...arr]; // create a copy of the array let step = Math.ceil(newArr.length / items.length); // Insert the first item at the beginning of the array newArr.unshift(items[0]); for (let i = 1; i < items.length; i++) { let position = i * step + 1; // +1 to account for the first item newArr.splice(position, 0, items[i]); } return newArr; } function sortAlphabetical(a, b) { if (a.data.name < b.data.name) { return -1; } if (a.data.name > b.data.name) { return 1; } return 0; } function sortByPopularity(a, b) { if (a.data.rank < b.data.rank) { return -1; } if (a.data.rank > b.data.rank) { return 1; } return 0; } const sortMap = { name: sortAlphabetical, popular: sortByPopularity, }; export default function HooksList({ hooks }) { const [sort, setSort] = useState<"name" | "popular">("popular"); const list = hooks.sort(sortMap[sort]); const listWithCallouts = insertAtIntervals(list, [ { id: "Callout 1", image: "d20", imageWidth: "222", imageHeight: "206", imageAlt: "20-sided die", pitch: "It’s dangerous to go alone! Master React by learning how to build useHooks yourself.", }, { id: "Callout 2", image: "spinner", imageWidth: "284", imageHeight: "180", imageAlt: "board game spinner and all options are React", pitch: "There’s no better way to learn useHooks than by building it yourself.", }, { id: "Callout 3", image: "money", imageWidth: "210", imageHeight: "210", imageAlt: "$100 Monopoly-style money", pitch: "Please give us your money.", }, { id: "Callout 4", image: "hot-sauce", imageWidth: "206", imageHeight: "224", imageAlt: "travel-style postcard from React that says “Enjoy the views!", pitch: "The all new interactive way to master modern React (for fun and profit).", } ]); return (
      {listWithCallouts.map( ({ data, id, image, imageWidth, imageHeight, imageAlt, pitch }) => { if (!data) { return ( ); } return ( ); } )}
    ); } ================================================ FILE: usehooks.com/src/content/config.ts ================================================ import { defineCollection, z, reference } from 'astro:content'; export const collections = { hooks: defineCollection({ schema: z.object({ experimental: z.boolean().optional(), draft: z.boolean().default(false), sandboxId: z.string().optional(), previewHeight: z.string().optional(), name: z.string(), tagline: z.string(), ogImage: z.string().optional(), rank: z.number(), relatedHooks: z.array( reference('hooks').optional() ).optional(), }), }), }; ================================================ FILE: usehooks.com/src/content/hooks/useBattery.mdx ================================================ --- name: useBattery rank: 32 tagline: Track the battery status of a user’s device with useBattery. sandboxId: usebattery-o8js1p previewHeight: 320px relatedHooks: - usenetworkstate - usepreferredlanguage --- import CodePreview from "../../components/CodePreview.astro"; import HookDescription from "../../components/HookDescription.astro"; import StaticCodeContainer from "../../components/StaticCodeContainer.astro"; The useBattery hook is useful for accessing and monitoring the battery status of the user’s device in a React application. By using this hook, you can easily retrieve information such as the battery level, charging status, and estimated charging and discharging times. It provides a state object that includes properties like supported, loading, level, charging, chargingTime, and dischargingTime.
    ### Return Values The hook returns an object containing the following properties:
    | Name | Type | Description | | ---------------- | ------- | ----------- | | supported | boolean | Indicates whether the Battery Status API is supported in the user’s browser. | | loading | boolean | Indicates if the battery information is still loading. | | level | number | Represents the level of the system’s battery. 0.0 means that the system’s battery is completely discharged, and 1.0 means the battery is completely charged. | | charging | boolean | Represents whether the system’s battery is charging. `true` means the battery is charging, `false` means it’s not. | | chargingTime | number | Represents the time remaining in seconds until the system’s battery is fully charged. | | dischargingTime | number | Represents the time remaining in seconds until the system’s battery is completely discharged and the system is about to be suspended. |
    ```jsx import { useBattery } from "@uidotdev/usehooks"; import Battery from "./Battery"; export default function App() { const { loading, level, charging, chargingTime, dischargingTime } = useBattery(); return ( <>

    useBattery

    {!loading ? ( ) : (

    Loading...

    )}
    ); } ```
    ================================================ FILE: usehooks.com/src/content/hooks/useClickAway.mdx ================================================ --- name: useClickAway rank: 47 tagline: Detect clicks outside of specific component with useClickAway. sandboxId: useclickaway-p4hl4m previewHeight: 250px relatedHooks: - uselongpress - usehover --- import CodePreview from "../../components/CodePreview.astro"; import HookDescription from "../../components/HookDescription.astro"; import StaticCodeContainer from "../../components/StaticCodeContainer.astro"; The useClickAway hook is a useful for detecting clicks outside a specific component. It allows you to pass a callback function that will be triggered whenever a click occurs outside the component’s area. This hook is particularly helpful when implementing dropdown menus, modals, or any other UI elements that need to be closed when the user clicks outside of them. By attaching event listeners to the document, the hook checks if the click target is within the component’s reference, and if not, it invokes the provided callback function.
    ### Parameters
    | Name | Type | Description | | ---- | -------- | ----------- | | callback | function | The callback function that is provided as an argument to `useClickAway`. This function is invoked whenever a click event is detected outside of the referenced element. The event object from the click is passed to this callback function. |
    ### Return Values
    | Name | Type | Description | | ---- | ------------ | ----------- | | ref | React ref | This is a ref object returned by the hook. It should be attached to a React element to monitor click events. The ref provides a way to access the properties of the element it is attached to. |
    ```jsx import * as React from "react"; import { useClickAway } from "@uidotdev/usehooks"; import { closeIcon } from "./icons"; export default function App() { const [isOpen, setIsOpen] = React.useState(false); const ref = useClickAway(() => { setIsOpen(false); }); const handleOpenModal = () => { if (isOpen === false) { setIsOpen(true); } }; return ( <>

    useClickAway

    {isOpen && (

    Modal

    Click outside the modal to close (or use the button) whatever you prefer.

    )} ); } ```
    ================================================ FILE: usehooks.com/src/content/hooks/useContinuousRetry.mdx ================================================ --- name: useContinuousRetry experimental: true rank: 12 tagline: Automates retries of a callback function until it succeeds with useContinuousRetry sandboxId: usecontinuousretry-v0uf1n previewHeight: 380px relatedHooks: - usesessionstorage --- import CodePreview from "../../components/CodePreview.astro"; import HookDescription from "../../components/HookDescription.astro"; import StaticCodeContainer from "../../components/StaticCodeContainer.astro"; The useContinuousRetry hook allows you to repeatedly call a specified callback function at a defined interval until the callback returns a truthy value, indicating a successful resolution. This hook is particularly handy when dealing with asynchronous operations or API calls that may fail temporarily and need to be retried automatically. It encapsulates the logic of retrying and provides a clean interface to handle retry-related states, such as whether the retry process has resolved or not.
    ### Parameters
    | Name | Type | Description | | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------| | callback | function | The callback function to be executed repeatedly until it returns a truthy value. | | interval | number | (Optional) The interval in milliseconds at which the callback function is executed. Default value is 100 milliseconds. | | options | object | (Optional) An object containing a `maxRetries` property which tells `useContinuousRetry` the maximum amount of retry attempts it should make before stopping |
    ### Return Value
    | Type | Description | | ------- | ------------------------------------------------------------------------------------------ | | boolean | `true` if the callback function has resolved (returned a truthy value), `false` otherwise. |
    ```jsx import * as React from "react"; import { useContinuousRetry } from "@uidotdev/usehooks"; export default function App() { const [count, setCount] = React.useState(0); const hasResolved = useContinuousRetry(() => { console.log("retrying"); return count > 10; }, 1000); return (

    useContinuousRetry

    {JSON.stringify({ hasResolved, count }, null, 2)}
    ); } ```
    ================================================ FILE: usehooks.com/src/content/hooks/useCopyToClipboard.mdx ================================================ --- name: useCopyToClipboard rank: 31 tagline: Copy text to the clipboard using useCopyToClipboard. sandboxId: usecopytoclipboard-y22r6w previewHeight: 300px relatedHooks: - usemap - usequeue --- import CodePreview from "../../components/CodePreview.astro"; import HookDescription from "../../components/HookDescription.astro"; import StaticCodeContainer from "../../components/StaticCodeContainer.astro"; The useCopyToClipboard hook is useful because it abstracts the complexity of copying text to the clipboard in a cross-browser compatible manner. It utilizes the modern navigator.clipboard.writeText method if available, which provides a more efficient and secure way to copy text. In case the writeText method is not supported by the browser, it falls back to a traditional method using the document.execCommand("copy") approach.
    ### Return Value The `useCopyToClipboard` hook returns an array with the following elements:
    | Index | Type | Description | | ----- | -------- | ------------------------------------------------------ | | 0 | string | The value that was last copied to the clipboard. | | 1 | function | A function to copy a specified value to the clipboard. |
    ```jsx import * as React from "react"; import { useCopyToClipboard } from "@uidotdev/usehooks"; import { copyIcon, checkIcon } from "./icons"; const randomHash = crypto.randomUUID(); export default function App() { const [copiedText, copyToClipboard] = useCopyToClipboard(); const hasCopiedText = Boolean(copiedText); return (

    useCopyToClipboard

              {randomHash}
              
            
    {hasCopiedText && (

    Copied{" "} 🎉