# hookli — full documentation for LLMs > hookli is a zero-dependency, fully-typed, SSR-safe React hooks library — 65 hooks for state, effects, the DOM and data. Install with `npm i hookli`. This file contains every hook's name, description, signature, parameters, returns, usage and source — the complete reference in one document. --- # useToggle > Boolean state with toggle and explicit set. - Category: State - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-toggle ## Signature ```ts useToggle(initialValue?: boolean): [boolean, () => void, (value: boolean) => void] ``` ## Parameters - `initialValue`: `boolean` (default: `false`) — The value the toggle starts from. ## Returns - `[0] value`: `boolean` — The current boolean state. - `[1] toggle`: `() => void` — Flips the value. - `[2] setValue`: `(value: boolean) => void` — Sets the value explicitly. ## Usage ```tsx import { useToggle } from "hookli"; export function Demo() { const [on, toggle, setOn] = useToggle(false); return (
); } ``` ## Source `src/hooks/useToggle.hook.ts` ```ts import { useCallback, useState } from "react"; export const useToggle = ( initialValue = false, ): [boolean, () => void, (value: boolean) => void] => { const [state, setState] = useState(initialValue); const toggle = useCallback(() => { setState((prevState) => !prevState); }, []); const setExplicit = useCallback((value: boolean) => { setState(value); }, []); return [state, toggle, setExplicit]; }; ``` --- # useForm > Controlled form state with one change handler. - Category: State - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-form ## Signature ```ts useForm(initialState: T): { values: T; handleChange: (e: ChangeEvent) => void; resetForm: () => void } ``` ## Parameters - `initialState`: `T` — Initial field values. Keys must match the name attribute of each input. ## Returns - `values`: `T` — The current form values. - `handleChange`: `(event: ChangeEvent) => void` — One change handler for every named input, textarea and select. - `resetForm`: `() => void` — Restores initialState. ## Usage ```tsx import { useForm } from "hookli"; export function Demo() { const { values, handleChange, resetForm } = useForm({ name: "", email: "" }); return (
e.preventDefault()}>
); } ``` ## Source `src/hooks/useForm.hook.ts` ```ts import { ChangeEvent, useState } from "react"; interface UseFormValues { [key: string]: string | number | boolean; } export const useForm = (initialState: T) => { const [values, setValues] = useState(initialState); const handleChange = ( event: ChangeEvent< HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement >, ) => { const { name, value } = event.target; setValues((prevValues) => ({ ...prevValues, [name]: value })); }; const resetForm = () => { setValues(initialState); }; return { values, handleChange, resetForm }; }; ``` --- # useLocalStorage > State persisted to localStorage. - Category: State - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-local-storage ## Signature ```ts useLocalStorage(key: string, initialValue: T): { value: T; setStoredValue: (value: T | ((val: T) => T)) => void } ``` ## Parameters - `key`: `string` — The localStorage key to read and write. - `initialValue`: `T` — Value used before hydration and when the key is empty. Prefer a stable reference — the sync effect depends on it. ## Returns - `value`: `T` — The stored value. Server-rendered as initialValue, then synced from localStorage after mount. - `setStoredValue`: `(value: T | ((val: T) => T)) => void` — Persists to localStorage and updates state; accepts a value or an updater function. ## Usage ```tsx import { useLocalStorage } from "hookli"; export function Demo() { const { value, setStoredValue } = useLocalStorage("note", ""); return (
setStoredValue(e.target.value)} />
); } ``` ## Source `src/hooks/useLocalStorage.hook.ts` ```ts import { useEffect, useState } from "react"; export const useLocalStorage = (key: string, initialValue: T) => { const [value, setValue] = useState(initialValue); useEffect(() => { const item = window.localStorage.getItem(key); const parsedValue = item ? JSON.parse(item) : initialValue; setValue(parsedValue); }, [key, initialValue]); const setStoredValue = (newValue: T | ((val: T) => T)) => { const updatedValue = newValue instanceof Function ? newValue(value) : newValue; window.localStorage.setItem(key, JSON.stringify(updatedValue)); setValue(updatedValue); }; return { value, setStoredValue }; }; ``` --- # useLocalStorageWithExpiry > Persisted state with a TTL. - Category: State - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-local-storage-with-expiry ## Signature ```ts useLocalStorageWithExpiry(key: string, initialValue: T, expiryMs: number): { value: T | null; setStoredValue: (value: T) => void } ``` ## Parameters - `key`: `string` — The localStorage key to read and write. - `initialValue`: `T` — Value used before hydration and when nothing is stored under the key. - `expiryMs`: `number` — Time-to-live in milliseconds. Every write stores the value with a fresh expiry timestamp. ## Returns - `value`: `T | null` — The stored value, or null once the item has expired. Expiry is checked when the hook reads — on mount or key change — at which point the item is removed. - `setStoredValue`: `(value: T) => void` — Persists the value to localStorage with a new expiry of now + expiryMs. ## Usage ```tsx import { useLocalStorageWithExpiry } from "hookli"; export function Demo() { const { value, setStoredValue } = useLocalStorageWithExpiry( "draft", "", 10_000, ); return (

{value === null ? "Expired" : value || "Nothing stored"}

); } ``` ## Source `src/hooks/useLocalStorageWithExpiry.hook.ts` ```ts import { useEffect, useState } from "react"; export const useLocalStorageWithExpiry = ( key: string, initialValue: T, expiryMs: number, ) => { const read = (): T | null => { if (typeof window === "undefined") return initialValue; const raw = window.localStorage.getItem(key); if (!raw) return initialValue; try { const item = JSON.parse(raw); if ( item && typeof item.expiry === "number" && Date.now() > item.expiry ) { window.localStorage.removeItem(key); return null; } return item.value; } catch { return initialValue; } }; const [value, setValue] = useState(initialValue); useEffect(() => { setValue(read()); }, [key]); const setStoredValue = (newValue: T) => { setValue(newValue); if (typeof window === "undefined") return; const item = { value: newValue, expiry: Date.now() + expiryMs }; window.localStorage.setItem(key, JSON.stringify(item)); }; return { value, setStoredValue }; }; ``` --- # useSessionStorage > useState backed by sessionStorage, synced across tabs. - Category: State - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-session-storage ## Signature ```ts useSessionStorage(key: string, initialValue: T | (() => T), options?: UseSessionStorageOptions): [T, (value: T | ((prev: T) => T)) => void, () => void] ``` ## Parameters - `key`: `string` — The sessionStorage key to read and write. - `initialValue`: `T | (() => T)` — Value used before hydration and when the key is empty. Pass a function to compute it lazily. - `options`: `UseSessionStorageOptions` (default: `{}`) — Optional custom serializer/deserializer and hydration flag. ## Returns - `[0] value`: `T` — The stored value. Server-rendered as initialValue, then hydrated from sessionStorage after mount. - `[1] setValue`: `(value: T | ((prev: T) => T)) => void` — Persists to sessionStorage and updates state; accepts a value or an updater. Syncs every hook using the key in this tab. - `[2] removeValue`: `() => void` — Removes the key from sessionStorage and resets state to initialValue. ## Types ### UseSessionStorageOptions Custom (de)serialization and hydration behaviour. - `serializer`: `(value: T) => string` — Turn the value into the string stored under the key. Defaults to JSON.stringify. - `deserializer`: `(value: string) => T` — Parse the stored string back into a value. Defaults to JSON.parse. - `initializeWithValue`: `boolean` (default: `true`) — Read sessionStorage synchronously on mount. Set false to defer to after hydration and avoid SSR mismatches. ## Usage ```tsx import { useSessionStorage } from "hookli"; export function Demo() { const [draft, setDraft, removeDraft] = useSessionStorage("draft", ""); return (
setDraft(e.target.value)} />
); } ``` ## Source `src/hooks/use-session-storage/use-session-storage.ts` ```ts import { useCallback, useEffect, useState } from "react"; import { useEventCallback } from "../use-event-callback/use-event-callback"; import { useEventListener } from "../use-event-listener/use-event-listener"; interface UseSessionStorageOptions { serializer?: (value: T) => string; deserializer?: (value: string) => T; initializeWithValue?: boolean; } type UseSessionStorageReturn = [ T, (value: T | ((prev: T) => T)) => void, () => void, ]; const IS_SERVER = typeof window === "undefined"; export function useSessionStorage( key: string, initialValue: T | (() => T), options: UseSessionStorageOptions = {}, ): UseSessionStorageReturn { const { initializeWithValue = true } = options; const serializer = useCallback( (value: T) => { if (options.serializer) return options.serializer(value); return JSON.stringify(value); }, [options], ); const deserializer = useCallback( (value: string): T => { if (options.deserializer) return options.deserializer(value); const defaultValue = initialValue instanceof Function ? initialValue() : initialValue; if (value === "undefined") return defaultValue; try { return JSON.parse(value); } catch { return defaultValue; } }, [options, initialValue], ); const readValue = useCallback((): T => { const initial = initialValue instanceof Function ? initialValue() : initialValue; if (IS_SERVER) return initial; try { const raw = window.sessionStorage.getItem(key); return raw ? deserializer(raw) : initial; } catch { return initial; } }, [initialValue, key, deserializer]); const [storedValue, setStoredValue] = useState(() => initializeWithValue ? readValue() : initialValue instanceof Function ? initialValue() : initialValue, ); const setValue = useEventCallback((value: T | ((prev: T) => T)) => { if (IS_SERVER) return; try { const newValue = value instanceof Function ? value(readValue()) : value; window.sessionStorage.setItem(key, serializer(newValue)); setStoredValue(newValue); window.dispatchEvent(new StorageEvent("session-storage", { key })); } catch { return; } }); const removeValue = useEventCallback(() => { if (IS_SERVER) return; const defaultValue = initialValue instanceof Function ? initialValue() : initialValue; window.sessionStorage.removeItem(key); setStoredValue(defaultValue); window.dispatchEvent(new StorageEvent("session-storage", { key })); }); useEffect(() => { setStoredValue(readValue()); }, [key]); const handleStorageChange = useCallback( (event: StorageEvent) => { if (event.key && event.key !== key) return; setStoredValue(readValue()); }, [key, readValue], ); useEventListener("storage", handleStorageChange); useEventListener("session-storage", handleStorageChange); return [storedValue, setValue, removeValue]; } ``` --- # useReadLocalStorage > Read a localStorage key without writing it, reactively. - Category: State - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-read-local-storage ## Signature ```ts useReadLocalStorage(key: string, options?: UseReadLocalStorageOptions): T | null ``` ## Parameters - `key`: `string` — The localStorage key to observe. - `options`: `UseReadLocalStorageOptions` (default: `{}`) — Optional custom deserializer and hydration flag. ## Returns - `value`: `T | null` — The parsed value, or null when the key is absent. Re-renders when the key changes in another tab (storage event) or via a local-storage event dispatched in this tab. ## Types ### UseReadLocalStorageOptions Custom deserialization and hydration behaviour. - `deserializer`: `(value: string) => T` — Parse the stored string into a value. Defaults to JSON.parse, falling back to the raw string. - `initializeWithValue`: `boolean` (default: `true`) — Read localStorage synchronously on mount. Set false to defer to after hydration and avoid SSR mismatches. ## Usage ```tsx import { useReadLocalStorage } from "hookli"; export function Demo() { const theme = useReadLocalStorage("theme"); return

Saved theme: {theme ?? "none"}

; } ``` ## Source `src/hooks/use-read-local-storage/use-read-local-storage.ts` ```ts import { useCallback, useEffect, useState } from "react"; import { useEventListener } from "../use-event-listener/use-event-listener"; interface UseReadLocalStorageOptions { deserializer?: (value: string) => T; initializeWithValue?: boolean; } const IS_SERVER = typeof window === "undefined"; export function useReadLocalStorage( key: string, options: UseReadLocalStorageOptions = {}, ): T | null { const { initializeWithValue = true } = options; const deserializer = useCallback( (value: string): T | undefined => { if (options.deserializer) return options.deserializer(value); if (value === "undefined") return undefined; try { return JSON.parse(value); } catch { return value as unknown as T; } }, [options], ); const readValue = useCallback((): T | null => { if (IS_SERVER) return null; try { const raw = window.localStorage.getItem(key); return raw ? (deserializer(raw) as T) : null; } catch { return null; } }, [key, deserializer]); const [storedValue, setStoredValue] = useState(() => initializeWithValue ? readValue() : null, ); useEffect(() => { setStoredValue(readValue()); }, [key]); const handleStorageChange = useCallback( (event: StorageEvent) => { if (event.key && event.key !== key) return; setStoredValue(readValue()); }, [key, readValue], ); useEventListener("storage", handleStorageChange); useEventListener("local-storage", handleStorageChange); return storedValue; } ``` --- # useDarkMode > Dark-mode boolean with toggle. - Category: State - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-dark-mode ## Signature ```ts useDarkMode(): { isDarkMode: boolean; toggleDarkMode: () => void } ``` ## Returns - `isDarkMode`: `boolean` — Current mode. Initialized from localStorage("theme") on the client; false during SSR. - `toggleDarkMode`: `() => void` — Flips the mode. An effect persists it to localStorage("theme") and toggles a "dark" class on . ## Usage ```tsx import { useDarkMode } from "hookli"; export function Demo() { const { isDarkMode, toggleDarkMode } = useDarkMode(); return (
); } ``` ## Source `src/hooks/useDarkMode.hook.ts` ```ts import { useEffect, useState } from "react"; interface UseDarkModeState { isDarkMode: boolean; toggleDarkMode: () => void; } export const useDarkMode = (): UseDarkModeState => { const [isDarkMode, setIsDarkMode] = useState(() => { if (typeof window === "undefined") return false; return window.localStorage.getItem("theme") === "dark"; }); const toggleDarkMode = () => setIsDarkMode((prevMode) => !prevMode); useEffect(() => { if (typeof document === "undefined") return; const bodyElement = document.body; const darkClass = "dark"; bodyElement.classList.toggle(darkClass, isDarkMode); window.localStorage.setItem("theme", isDarkMode ? "dark" : "light"); return () => { bodyElement.classList.remove(darkClass); }; }, [isDarkMode]); return { isDarkMode, toggleDarkMode }; }; ``` --- # useTernaryDarkMode > Three-state dark mode — system, dark or light — persisted. - Category: State - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-ternary-dark-mode ## Signature ```ts useTernaryDarkMode(options?: UseTernaryDarkModeOptions): UseTernaryDarkModeReturn ``` ## Parameters - `options`: `UseTernaryDarkModeOptions` (default: `{}`) — Optional starting mode and localStorage key. ## Returns - `{ … }`: `UseTernaryDarkModeReturn` — The resolved isDarkMode boolean, the stored ternary preference, and its setters. ## Types ### TernaryDarkMode The three possible preferences. - `value`: `"system" | "dark" | "light"` — "system" resolves against the OS; "dark"/"light" force a mode. ### UseTernaryDarkModeOptions Optional starting mode and persistence key. - `defaultValue`: `TernaryDarkMode` (default: `"system"`) — The mode before anything is stored: "system", "dark" or "light". - `localStorageKey`: `string` (default: `"hookli-ternary-dark-mode"`) — The localStorage key the choice is persisted under. ### UseTernaryDarkModeReturn The resolved mode plus setters. - `isDarkMode`: `boolean` — True when the mode is "dark", or "system" while the OS prefers dark. - `ternaryDarkMode`: `TernaryDarkMode` — The stored preference: "system" | "dark" | "light". - `setTernaryDarkMode`: `(value: TernaryDarkMode | ((prev: TernaryDarkMode) => TernaryDarkMode)) => void` — Set the preference directly; accepts a value or an updater. - `toggleTernaryDarkMode`: `() => void` — Cycle the preference: light → system → dark → light. ## Usage ```tsx import { useTernaryDarkMode } from "hookli"; export function Demo() { const { isDarkMode, ternaryDarkMode, setTernaryDarkMode } = useTernaryDarkMode(); return (
); } ``` ## Source `src/hooks/use-ternary-dark-mode/use-ternary-dark-mode.ts` ```ts import { useCallback } from "react"; import { useLocalStorage } from "../use-local-storage/use-local-storage"; import { useMediaQuery } from "../use-media-query/use-media-query"; type TernaryDarkMode = "system" | "dark" | "light"; interface UseTernaryDarkModeOptions { defaultValue?: TernaryDarkMode; localStorageKey?: string; } interface UseTernaryDarkModeReturn { isDarkMode: boolean; ternaryDarkMode: TernaryDarkMode; setTernaryDarkMode: ( value: TernaryDarkMode | ((prev: TernaryDarkMode) => TernaryDarkMode), ) => void; toggleTernaryDarkMode: () => void; } const COLOR_SCHEME_QUERY = "(prefers-color-scheme: dark)"; const DEFAULT_STORAGE_KEY = "hookli-ternary-dark-mode"; export function useTernaryDarkMode( options: UseTernaryDarkModeOptions = {}, ): UseTernaryDarkModeReturn { const { defaultValue = "system", localStorageKey = DEFAULT_STORAGE_KEY } = options; const isDarkOS = useMediaQuery(COLOR_SCHEME_QUERY); const { value: ternaryDarkMode, setStoredValue: setTernaryDarkMode } = useLocalStorage(localStorageKey, defaultValue); const isDarkMode = ternaryDarkMode === "dark" || (ternaryDarkMode === "system" && isDarkOS); const toggleTernaryDarkMode = useCallback(() => { const cycle: TernaryDarkMode[] = ["light", "system", "dark"]; setTernaryDarkMode((prev) => { const nextIndex = (cycle.indexOf(prev) + 1) % cycle.length; return cycle[nextIndex]; }); }, [setTernaryDarkMode]); return { isDarkMode, ternaryDarkMode, setTernaryDarkMode, toggleTernaryDarkMode, }; } ``` --- # useBoolean > Boolean state with setTrue, setFalse, toggle and set. - Category: State - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-boolean ## Signature ```ts useBoolean(defaultValue?: boolean): { value: boolean; setValue: (value: boolean) => void; setTrue: () => void; setFalse: () => void; toggle: () => void } ``` ## Parameters - `defaultValue`: `boolean` (default: `false`) — The value the boolean starts from. ## Returns - `value`: `boolean` — The current boolean value. - `setValue`: `(value: boolean) => void` — Sets the value directly. - `setTrue`: `() => void` — Sets the value to true. - `setFalse`: `() => void` — Sets the value to false. - `toggle`: `() => void` — Flips the value. ## Usage ```tsx import { useBoolean } from "hookli"; export function Demo() { const { value, setTrue, setFalse, toggle } = useBoolean(false); return (

{value ? "On" : "Off"}

); } ``` ## Source `src/hooks/use-boolean/use-boolean.ts` ```ts import { useCallback, useState } from "react"; export interface UseBooleanReturn { value: boolean; setValue: (value: boolean) => void; setTrue: () => void; setFalse: () => void; toggle: () => void; } export const useBoolean = (defaultValue = false): UseBooleanReturn => { const [value, setValue] = useState(defaultValue); const setTrue = useCallback(() => setValue(true), []); const setFalse = useCallback(() => setValue(false), []); const toggle = useCallback(() => setValue((prev) => !prev), []); return { value, setValue, setTrue, setFalse, toggle }; }; ``` --- # useCounter > Numeric counter with increment, decrement and reset. - Category: State - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-counter ## Signature ```ts useCounter(initialValue?: number): { count: number; increment: () => void; decrement: () => void; reset: () => void; setCount: Dispatch> } ``` ## Parameters - `initialValue`: `number` (default: `0`) — The count the hook starts from; reset returns here. ## Returns - `count`: `number` — The current count. - `increment`: `() => void` — Adds one to the count. - `decrement`: `() => void` — Subtracts one from the count. - `reset`: `() => void` — Restores the count to initialValue. - `setCount`: `Dispatch>` — Sets the count directly; accepts a value or an updater function. ## Usage ```tsx import { useCounter } from "hookli"; export function Demo() { const { count, increment, decrement, reset, setCount } = useCounter(0); return (

{count}

); } ``` ## Source `src/hooks/use-counter/use-counter.ts` ```ts import { Dispatch, SetStateAction, useCallback, useState } from "react"; export interface UseCounterReturn { count: number; increment: () => void; decrement: () => void; reset: () => void; setCount: Dispatch>; } export const useCounter = (initialValue = 0): UseCounterReturn => { const [count, setCount] = useState(initialValue); const increment = useCallback(() => setCount((prev) => prev + 1), []); const decrement = useCallback(() => setCount((prev) => prev - 1), []); const reset = useCallback(() => setCount(initialValue), [initialValue]); return { count, increment, decrement, reset, setCount }; }; ``` --- # useStep > 1-indexed step counter for wizards and steppers. - Category: State - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-step ## Signature ```ts useStep(maxStep: number): [number, UseStepActions] ``` ## Parameters - `maxStep`: `number` — The highest reachable step (inclusive). Steps run from 1 to maxStep. ## Returns - `[0] step`: `number` — The current step, 1-indexed. - `[1] actions`: `UseStepActions` — Controls for moving between steps — see below. ## Types ### UseStepActions The second tuple element — the stepper controls. - `goToNextStep`: `() => void` — Advances to the next step; a no-op at maxStep. - `goToPrevStep`: `() => void` — Goes back one step; a no-op at step 1. - `reset`: `() => void` — Resets back to step 1. - `canGoToNextStep`: `boolean` — Whether a next step is available. - `canGoToPrevStep`: `boolean` — Whether a previous step is available. - `setStep`: `Dispatch>` — Sets the step directly (1-indexed); throws if outside the 1..maxStep range. ## Usage ```tsx import { useStep } from "hookli"; export function Demo() { const [step, { goToNextStep, goToPrevStep, canGoToNextStep, canGoToPrevStep, reset }] = useStep(4); return (

Step {step} of 4

); } ``` ## Source `src/hooks/use-step/use-step.ts` ```ts import { Dispatch, SetStateAction, useCallback, useMemo, useState } from "react"; export interface UseStepActions { goToNextStep: () => void; goToPrevStep: () => void; reset: () => void; canGoToNextStep: boolean; canGoToPrevStep: boolean; setStep: Dispatch>; } export const useStep = (maxStep: number): [number, UseStepActions] => { const [currentStep, setCurrentStep] = useState(1); const canGoToNextStep = useMemo( () => currentStep + 1 <= maxStep, [currentStep, maxStep], ); const canGoToPrevStep = useMemo(() => currentStep - 1 >= 1, [currentStep]); const setStep = useCallback>>( (step) => { setCurrentStep((prev) => { const newStep = step instanceof Function ? step(prev) : step; if (newStep >= 1 && newStep <= maxStep) { return newStep; } throw new Error("Step not valid"); }); }, [maxStep], ); const goToNextStep = useCallback(() => { setCurrentStep((prev) => (prev + 1 <= maxStep ? prev + 1 : prev)); }, [maxStep]); const goToPrevStep = useCallback(() => { setCurrentStep((prev) => (prev - 1 >= 1 ? prev - 1 : prev)); }, []); const reset = useCallback(() => { setCurrentStep(1); }, []); return [ currentStep, { goToNextStep, goToPrevStep, canGoToNextStep, canGoToPrevStep, setStep, reset }, ]; }; ``` --- # useCountdown > Self-stopping countdown or count-up timer. - Category: State - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-countdown ## Signature ```ts useCountdown(options: UseCountdownOptions): [number, UseCountdownActions] ``` ## Parameters - `options`: `UseCountdownOptions` — Configures the timer — start value, tick interval, direction and stop value. See below. ## Returns - `[0] count`: `number` — The current count. Ticks by ±1 every intervalMs while running. - `[1] actions`: `UseCountdownActions` — Start, pause and reset controls — see below. ## Types ### UseCountdownOptions The single options argument. - `countStart`: `number` — The value the countdown starts from. - `intervalMs`: `number` — Milliseconds between ticks. Defaults to 1000. - `isIncrement`: `boolean` — Count up instead of down. Defaults to false. - `countStop`: `number` — The value at which the timer stops itself. Defaults to 0. ### UseCountdownActions The second tuple element — the timer controls. - `startCountdown`: `() => void` — Starts (or resumes) the timer. - `stopCountdown`: `() => void` — Pauses the timer without resetting the count. - `resetCountdown`: `() => void` — Stops the timer and resets the count to countStart. ## Usage ```tsx import { useCountdown } from "hookli"; export function Demo() { const [count, { startCountdown, stopCountdown, resetCountdown }] = useCountdown({ countStart: 10, intervalMs: 1000, }); return (

{count}

); } ``` ## Source `src/hooks/use-countdown/use-countdown.ts` ```ts import { useCallback, useEffect, useRef, useState } from "react"; export interface UseCountdownOptions { countStart: number; intervalMs?: number; isIncrement?: boolean; countStop?: number; } export interface UseCountdownActions { startCountdown: () => void; stopCountdown: () => void; resetCountdown: () => void; } export const useCountdown = ({ countStart, intervalMs = 1000, isIncrement = false, countStop = 0, }: UseCountdownOptions): [number, UseCountdownActions] => { const [count, setCount] = useState(countStart); const [isRunning, setIsRunning] = useState(false); const startCountdown = useCallback(() => setIsRunning(true), []); const stopCountdown = useCallback(() => setIsRunning(false), []); const resetCountdown = useCallback(() => { setIsRunning(false); setCount(countStart); }, [countStart]); const tick = useRef(() => {}); tick.current = () => { setCount((prev) => (isIncrement ? prev + 1 : prev - 1)); }; useEffect(() => { if (isRunning && count === countStop) { setIsRunning(false); } }, [count, countStop, isRunning]); useEffect(() => { if (!isRunning) return; const id = setInterval(() => tick.current(), intervalMs); return () => clearInterval(id); }, [isRunning, intervalMs]); return [count, { startCountdown, stopCountdown, resetCountdown }]; }; ``` --- # useMap > Manage a Map as immutable React state. - Category: State - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-map ## Signature ```ts useMap(initialState?: MapOrEntries): [ReadOnlyMap, UseMapActions] ``` ## Parameters - `initialState`: `MapOrEntries` (default: `new Map()`) — Initial entries as a Map or an array of [key, value] pairs. ## Returns - `[0] map`: `ReadOnlyMap` — A read-only view of the map — the mutating set/clear/delete methods are omitted; use the actions instead. get, has, size and iteration remain. - `[1] actions`: `UseMapActions` — Stable helpers that replace the map with a fresh copy so React re-renders — see below. ## Types ### UseMapActions The second tuple element — the map mutation helpers. - `set`: `(key: K, value: V) => void` — Adds or updates one entry. - `setAll`: `(entries: MapOrEntries) => void` — Replaces every entry with the given Map or [key, value] pairs. - `remove`: `(key: K) => void` — Deletes the entry for the given key. - `reset`: `() => void` — Empties the map. ## Usage ```tsx import { useMap } from "hookli"; export function Demo() { const [map, { set, remove, reset }] = useMap([ ["theme", "dark"], ]); return (
    {[...map.entries()].map(([key, value]) => (
  • {key}: {value}
  • ))}
); } ``` ## Source `src/hooks/use-map/use-map.ts` ```ts import { useCallback, useState } from "react"; export type MapOrEntries = Map | [K, V][]; export interface UseMapActions { set: (key: K, value: V) => void; setAll: (entries: MapOrEntries) => void; remove: (key: K) => void; reset: () => void; } export type ReadOnlyMap = Omit, "set" | "clear" | "delete">; export type UseMapReturn = [ReadOnlyMap, UseMapActions]; export function useMap( initialState: MapOrEntries = new Map(), ): UseMapReturn { const [map, setMap] = useState(() => new Map(initialState)); const set = useCallback((key: K, value: V) => { setMap((prev) => { const next = new Map(prev); next.set(key, value); return next; }); }, []); const setAll = useCallback((entries: MapOrEntries) => { setMap(new Map(entries)); }, []); const remove = useCallback((key: K) => { setMap((prev) => { const next = new Map(prev); next.delete(key); return next; }); }, []); const reset = useCallback(() => { setMap(new Map()); }, []); return [map, { set, setAll, remove, reset }]; } ``` --- # useDebounce > Debounces a changing value. - Category: Effects - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-debounce ## Signature ```ts useDebounce(value: T, delay: number): T ``` ## Parameters - `value`: `T` — The value to debounce — any type works. - `delay`: `number` — Milliseconds the value must stay unchanged before the debounced value updates. ## Returns - `debouncedValue`: `T` — Trails the input value, updating only after delay ms without a change. ## Usage ```tsx import { useState } from "react"; import { useDebounce } from "hookli"; export function Demo() { const [text, setText] = useState(""); const debounced = useDebounce(text, 500); return (
setText(e.target.value)} />

Value: {text}

Debounced: {debounced}

); } ``` ## Source `src/hooks/useDebounce.hook.ts` ```ts import { useEffect, useState } from "react"; export const useDebounce = (value: T, delay: number): T => { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const timer = setTimeout(() => { setDebouncedValue(value); }, delay); return () => { clearTimeout(timer); }; }, [value, delay]); return debouncedValue; }; ``` --- # useDebounceValue > State whose debounced copy updates after a pause. - Category: Effects - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-debounce-value ## Signature ```ts useDebounceValue(initialValue: T | (() => T), delayMs?: number, options?: DebounceOptions & { equalityFn?: (left: T, right: T) => boolean }): [T, DebouncedState<[T | ((prev: T) => T)], void>] ``` ## Parameters - `initialValue`: `T | (() => T)` — The starting value, or a factory evaluated once on mount — the same lazy-initializer contract as useState. - `delayMs`: `number` (default: `500`) — Milliseconds of inactivity before the debounced value catches up to the latest set value. - `options`: `DebounceOptions & { equalityFn? }` (default: `{}`) — DebounceOptions plus an optional equalityFn (left, right) => boolean — when it reports the values equal, the debounced update is skipped. See the tables below. ## Returns - `[0] debouncedValue`: `T` — The value that trails the setter, updating only after delayMs of quiet. - `[1] setValue`: `DebouncedState<[T | ((prev: T) => T)], void>` — A debounced setter accepting a value or updater; it also carries cancel, flush and isPending (see DebouncedState). ## Types ### DebounceOptions Controls how the debounced invocation is scheduled. - `leading`: `boolean` (default: `false`) — Invoke on the leading edge — run once immediately on the first call of a burst. - `trailing`: `boolean` (default: `true`) — Invoke on the trailing edge — run after the burst settles. - `maxWait`: `number` — The maximum time the callback may be delayed before it is forced to run, even during a continuous burst. ### DebouncedState The debounced function, plus manual control methods. - `(...args)`: `(...args: Args) => R | undefined` — Calling it schedules an invocation and returns the last computed result — undefined before the first run. - `cancel`: `() => void` — Cancels any pending trailing invocation. - `flush`: `() => R | undefined` — Immediately runs any pending invocation and returns its result. - `isPending`: `() => boolean` — Whether a trailing invocation is currently scheduled. ## Usage ```tsx import { useState } from "react"; import { useDebounceValue } from "hookli"; export function Demo() { const [text, setText] = useState(""); const [debounced, setValue] = useDebounceValue("", 500); return (
{ setText(e.target.value); setValue(e.target.value); }} />

Debounced: {debounced}

); } ``` ## Source `src/hooks/use-debounce-value/use-debounce-value.ts` ```ts import { useEffect, useRef, useState } from "react"; import { useDebounceCallback, type DebounceOptions, type DebouncedState, } from "../use-debounce-callback/use-debounce-callback"; export type UseDebounceValueReturn = [ T, DebouncedState<[value: T | ((prev: T) => T)], void>, ]; export const useDebounceValue = ( initialValue: T | (() => T), delayMs = 500, options: DebounceOptions & { equalityFn?: (left: T, right: T) => boolean } = {}, ): UseDebounceValueReturn => { const eq = options.equalityFn ?? ((left, right) => left === right); const unwrap = (v: T | (() => T)): T => typeof v === "function" ? (v as () => T)() : v; const [debouncedValue, setDebouncedValue] = useState(() => unwrap(initialValue), ); const previousValue = useRef(debouncedValue); const updateDebouncedValue = useDebounceCallback( (value: T | ((prev: T) => T)) => { const next = typeof value === "function" ? (value as (prev: T) => T)(previousValue.current) : value; if (!eq(previousValue.current, next)) { previousValue.current = next; setDebouncedValue(next); } }, delayMs, options, ); useEffect(() => { return () => updateDebouncedValue.cancel(); }, [updateDebouncedValue]); return [debouncedValue, updateDebouncedValue]; }; ``` --- # useDebounceCallback > Debounces a callback, with cancel, flush and isPending. - Category: Effects - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-debounce-callback ## Signature ```ts useDebounceCallback(fn: (...args: Args) => R, delayMs?: number, options?: DebounceOptions): DebouncedState ``` ## Parameters - `fn`: `(...args: Args) => R` — The function to debounce. The returned function keeps a stable identity across renders but always invokes the latest fn. - `delayMs`: `number` (default: `500`) — Milliseconds to wait after the last call before fn runs. - `options`: `DebounceOptions` (default: `{}`) — Leading/trailing edge and maxWait behaviour. See the table below. ## Returns - `debounced`: `DebouncedState` — A debounced version of fn with a stable identity, carrying cancel, flush and isPending. Any pending call is cancelled on unmount. ## Types ### DebounceOptions Controls how the debounced invocation is scheduled. - `leading`: `boolean` (default: `false`) — Invoke on the leading edge — run once immediately on the first call of a burst. - `trailing`: `boolean` (default: `true`) — Invoke on the trailing edge — run after the burst settles. - `maxWait`: `number` — The maximum time the callback may be delayed before it is forced to run, even during a continuous burst. ### DebouncedState The debounced function, plus manual control methods. - `(...args)`: `(...args: Args) => R | undefined` — Calling it schedules an invocation and returns the last computed result — undefined before the first run. - `cancel`: `() => void` — Cancels any pending trailing invocation. - `flush`: `() => R | undefined` — Immediately runs any pending invocation and returns its result. - `isPending`: `() => boolean` — Whether a trailing invocation is currently scheduled. ## Usage ```tsx import { useState } from "react"; import { useDebounceCallback } from "hookli"; export function Demo() { const [query, setQuery] = useState(""); const search = useDebounceCallback((q: string) => { console.log("searching", q); }, 600); return ( { setQuery(e.target.value); search(e.target.value); }} /> ); } ``` ## Source `src/hooks/use-debounce-callback/use-debounce-callback.ts` ```ts import { useEffect, useMemo, useRef } from "react"; import { useEventCallback } from "../use-event-callback/use-event-callback"; import { useUnmount } from "../use-unmount/use-unmount"; export interface DebounceOptions { /** Invoke on the leading edge of the timeout. Defaults to `false`. */ leading?: boolean; /** Invoke on the trailing edge of the timeout. Defaults to `true`. */ trailing?: boolean; /** Maximum time the callback may be delayed before it is forced to run. */ maxWait?: number; } export interface DebouncedState { (...args: Args): R | undefined; cancel: () => void; flush: () => R | undefined; isPending: () => boolean; } export const useDebounceCallback = ( fn: (...args: Args) => R, delayMs = 500, options: DebounceOptions = {}, ): DebouncedState => { const timeoutId = useRef>(); const maxTimeoutId = useRef>(); const lastArgs = useRef(); const lastResult = useRef(); const lastInvokeTime = useRef(0); const { leading = false, trailing = true, maxWait } = options; const latestFn = useEventCallback(fn); useEffect(() => { lastInvokeTime.current = 0; }, [delayMs, leading, trailing, maxWait]); const debounced = useMemo(() => { const invoke = (): R | undefined => { const args = lastArgs.current; if (!args) return undefined; lastArgs.current = undefined; lastInvokeTime.current = Date.now(); lastResult.current = latestFn(...args); return lastResult.current; }; const clearTimers = () => { if (timeoutId.current !== undefined) { clearTimeout(timeoutId.current); timeoutId.current = undefined; } if (maxTimeoutId.current !== undefined) { clearTimeout(maxTimeoutId.current); maxTimeoutId.current = undefined; } }; const trailingEdge = () => { clearTimers(); if (trailing && lastArgs.current) invoke(); else lastArgs.current = undefined; }; const state = Object.assign( (...args: Args): R | undefined => { lastArgs.current = args; const isFirstCall = timeoutId.current === undefined && maxTimeoutId.current === undefined; if (timeoutId.current !== undefined) clearTimeout(timeoutId.current); if (leading && isFirstCall) invoke(); timeoutId.current = setTimeout(trailingEdge, delayMs); if (maxWait !== undefined && maxTimeoutId.current === undefined) { maxTimeoutId.current = setTimeout(() => { clearTimers(); if (lastArgs.current) invoke(); }, maxWait); } return lastResult.current; }, { cancel: () => { clearTimers(); lastArgs.current = undefined; }, flush: (): R | undefined => { if (timeoutId.current === undefined) return lastResult.current; clearTimers(); return lastArgs.current ? invoke() : lastResult.current; }, isPending: () => timeoutId.current !== undefined && lastArgs.current !== undefined, }, ); return state; }, [delayMs, leading, trailing, maxWait, latestFn]); useUnmount(() => { debounced.cancel(); }); return debounced; }; ``` --- # useInterval > Runs a callback on a fixed interval; pause by passing null. - Category: Effects - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-interval ## Signature ```ts useInterval(callback: () => void, delay: number | null): void ``` ## Parameters - `callback`: `() => void` — Runs on every tick. Always fires the latest callback — the hook keeps a ref, so you never re-arm the timer just to close over fresh state. - `delay`: `number | null` — Milliseconds between ticks. Pass null to pause — the interval is cleared, so nothing runs until you set a number again. ## Usage ```tsx import { useState } from "react"; import { useInterval } from "hookli"; export function Demo() { const [count, setCount] = useState(0); const [running, setRunning] = useState(true); useInterval(() => setCount((prev) => prev + 1), running ? 1000 : null); return (

{count}

); } ``` ## Source `src/hooks/use-interval/use-interval.ts` ```ts import { useEffect, useRef } from "react"; export const useInterval = (callback: () => void, delay: number | null) => { const savedCallback = useRef(callback); useEffect(() => { savedCallback.current = callback; }, [callback]); useEffect(() => { if (delay === null) return; const id = setInterval(() => savedCallback.current(), delay); return () => clearInterval(id); }, [delay]); }; ``` --- # useTimeout > Runs a callback once after a delay; cancel by passing null. - Category: Effects - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-timeout ## Signature ```ts useTimeout(callback: () => void, delay: number | null): void ``` ## Parameters - `callback`: `() => void` — Runs once after the delay elapses. The hook keeps a ref to the latest callback, so it always fires with fresh state. - `delay`: `number | null` — Milliseconds to wait before firing. Pass null to disable — a change to null before the delay elapses cancels the pending timeout. ## Usage ```tsx import { useState } from "react"; import { useTimeout } from "hookli"; export function Demo() { const [visible, setVisible] = useState(false); const [delay, setDelay] = useState(null); useTimeout(() => { setVisible(true); setDelay(null); }, delay); return (
{visible &&

Here!

}
); } ``` ## Source `src/hooks/use-timeout/use-timeout.ts` ```ts import { useEffect, useRef } from "react"; export const useTimeout = (callback: () => void, delay: number | null) => { const savedCallback = useRef(callback); useEffect(() => { savedCallback.current = callback; }, [callback]); useEffect(() => { if (delay === null) return; const id = setTimeout(() => savedCallback.current(), delay); return () => clearTimeout(id); }, [delay]); }; ``` --- # useIsomorphicLayoutEffect > useLayoutEffect on the client, useEffect on the server. - Category: Effects - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-isomorphic-layout-effect ## Signature ```ts useIsomorphicLayoutEffect(effect: EffectCallback, deps?: DependencyList): void ``` ## Parameters - `effect`: `EffectCallback` — The effect to run — same contract as React's useLayoutEffect, including an optional cleanup return. - `deps`: `DependencyList` — Dependency array controlling when the effect re-runs. Omit to run after every render. ## Usage ```tsx import { useRef, useState } from "react"; import { useIsomorphicLayoutEffect } from "hookli"; export function Demo() { const boxRef = useRef(null); const [width, setWidth] = useState(0); useIsomorphicLayoutEffect(() => { setWidth(boxRef.current?.offsetWidth ?? 0); }, []); return
Measured: {width}px
; } ``` ## Source `src/hooks/use-isomorphic-layout-effect/use-isomorphic-layout-effect.ts` ```ts import { useEffect, useLayoutEffect } from "react"; /** * useLayoutEffect that safely falls back to useEffect on the server, where * useLayoutEffect would warn. Picks the layout effect only when a DOM exists. */ export const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect; ``` --- # useEventCallback > A stable callback that always calls the latest closure. - Category: Effects - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-event-callback ## Signature ```ts useEventCallback(fn: (...args: Args) => R): (...args: Args) => R ``` ## Parameters - `fn`: `(...args: Args) => R` — The function to keep current behind a stable reference. Calling during render throws — it is meant for event handlers and effects. ## Returns - `callback`: `(...args: Args) => R` — A memoized callback with an unchanging identity that always forwards to the latest fn. ## Usage ```tsx import { useState } from "react"; import { useEventCallback } from "hookli"; export function Demo() { const [count, setCount] = useState(0); const readLatest = useEventCallback(() => count); return (
); } ``` ## Source `src/hooks/use-event-callback/use-event-callback.ts` ```ts import { useCallback, useRef } from "react"; import { useIsomorphicLayoutEffect } from "../use-isomorphic-layout-effect/use-isomorphic-layout-effect"; export const useEventCallback = ( fn: (...args: Args) => R, ) => { const ref = useRef<(...args: Args) => R>(() => { throw new Error("Cannot call an event handler while rendering."); }); useIsomorphicLayoutEffect(() => { ref.current = fn; }, [fn]); return useCallback((...args: Args) => ref.current(...args), [ref]); }; ``` --- # useUnmount > Runs a cleanup function once, when the component unmounts. - Category: Effects - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-unmount ## Signature ```ts useUnmount(fn: () => void): void ``` ## Parameters - `fn`: `() => void` — Called exactly once when the component unmounts. The latest closure is captured in a ref, so it always sees fresh values. ## Usage ```tsx import { useUnmount } from "hookli"; export function Demo() { useUnmount(() => { console.log("cleanup on unmount"); }); return

Watch the console when I unmount.

; } ``` ## Source `src/hooks/use-unmount/use-unmount.ts` ```ts import { useEffect, useRef } from "react"; export const useUnmount = (fn: () => void) => { const fnRef = useRef(fn); // The latest closure every render, but only invoked on unmount. fnRef.current = fn; useEffect(() => () => fnRef.current(), []); }; ``` --- # useIsClient > Reports false on the server and true after hydration. - Category: Effects - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-is-client ## Signature ```ts useIsClient(): boolean ``` ## Returns - `isClient`: `boolean` — false during server rendering and the first hydration pass, then true once mounted in the browser. Gate browser-only UI on it to keep both renders identical. ## Usage ```tsx import { useIsClient } from "hookli"; export function Demo() { const isClient = useIsClient(); if (!isClient) return

Rendering on the server…

; return

Now running in the browser.

; } ``` ## Source `src/hooks/use-is-client/use-is-client.ts` ```ts import { useEffect, useState } from "react"; export const useIsClient = () => { const [isClient, setIsClient] = useState(false); useEffect(() => { setIsClient(true); }, []); return isClient; }; ``` --- # useIsMounted > A stable getter for whether the component is still mounted. - Category: Effects - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-is-mounted ## Signature ```ts useIsMounted(): () => boolean ``` ## Returns - `isMounted`: `() => boolean` — A stable getter that returns true while mounted and false after unmount. Call it inside async callbacks before setting state; its identity never changes, so it is safe to omit from dependency arrays. ## Usage ```tsx import { useIsMounted } from "hookli"; export function Demo() { const isMounted = useIsMounted(); async function load() { const data = await fetchData(); if (isMounted()) setData(data); } return ; } ``` ## Source `src/hooks/use-is-mounted/use-is-mounted.ts` ```ts import { useCallback, useEffect, useRef } from "react"; export const useIsMounted = () => { const isMounted = useRef(false); useEffect(() => { isMounted.current = true; return () => { isMounted.current = false; }; }, []); return useCallback(() => isMounted.current, []); }; ``` --- # useDocumentTitle > Keeps document.title in sync with a value, SSR-safe. - Category: Effects - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-document-title ## Signature ```ts useDocumentTitle(title: string, options?: UseDocumentTitleOptions): void ``` ## Parameters - `title`: `string` — The document title to apply. Written to document.title in a layout effect on the client and skipped during server rendering. - `options`: `UseDocumentTitleOptions` (default: `{}`) — Behaviour options (see below). ## Types ### UseDocumentTitleOptions Options controlling unmount behaviour. - `preserveTitleOnUnmount`: `boolean` (default: `true`) — When false, the title captured on mount is restored when the component unmounts. Defaults to true, which leaves the title in place. ## Usage ```tsx import { useDocumentTitle } from "hookli"; export function Demo() { useDocumentTitle("Dashboard — hookli", { preserveTitleOnUnmount: false, }); return

Dashboard

; } ``` ## Source `src/hooks/use-document-title/use-document-title.ts` ```ts import { useRef } from "react"; import { useIsomorphicLayoutEffect } from "../use-isomorphic-layout-effect/use-isomorphic-layout-effect"; import { useUnmount } from "../use-unmount/use-unmount"; interface UseDocumentTitleOptions { preserveTitleOnUnmount?: boolean; } export const useDocumentTitle = ( title: string, options: UseDocumentTitleOptions = {}, ) => { const { preserveTitleOnUnmount = true } = options; const defaultTitle = useRef(null); useIsomorphicLayoutEffect(() => { defaultTitle.current = window.document.title; }, []); useIsomorphicLayoutEffect(() => { window.document.title = title; }, [title]); useUnmount(() => { if (!preserveTitleOnUnmount && defaultTitle.current !== null) { window.document.title = defaultTitle.current; } }); }; ``` --- # useEventListener > Subscribe to a window, document or element event with cleanup. - Category: DOM - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-event-listener ## Signature ```ts useEventListener(eventName: K, handler: (event: Event) => void, element?: RefObject, options?: boolean | AddEventListenerOptions): void ``` ## Parameters - `eventName`: `K` — The event to listen for, typed against the target's event map (window, document, media query, or element). - `handler`: `(event) => void` — Called with the typed event on every dispatch. Held in a ref, so updating it never detaches and re-attaches the listener. - `element`: `RefObject` (default: `window`) — Optional ref to the target. Defaults to window when omitted. - `options`: `boolean | AddEventListenerOptions` — Standard addEventListener options (capture, passive, once). ## Usage ```tsx import { useRef, useState } from "react"; import { useEventListener } from "hookli"; export function Demo() { const ref = useRef(null); const [lastKey, setLastKey] = useState(""); useEventListener("keydown", (event) => setLastKey(event.key)); useEventListener("click", () => console.log("clicked"), ref); return
Last key: {lastKey}
; } ``` ## Source `src/hooks/use-event-listener/use-event-listener.ts` ```ts import { RefObject, useEffect, useRef } from "react"; import { useIsomorphicLayoutEffect } from "../use-isomorphic-layout-effect/use-isomorphic-layout-effect"; function useEventListener( eventName: K, handler: (event: MediaQueryListEventMap[K]) => void, element: RefObject, options?: boolean | AddEventListenerOptions, ): void; function useEventListener( eventName: K, handler: (event: WindowEventMap[K]) => void, element?: undefined, options?: boolean | AddEventListenerOptions, ): void; function useEventListener( eventName: K, handler: (event: DocumentEventMap[K]) => void, element: RefObject, options?: boolean | AddEventListenerOptions, ): void; function useEventListener< K extends keyof HTMLElementEventMap & keyof SVGElementEventMap, T extends HTMLElement | SVGElement = HTMLDivElement, >( eventName: K, handler: (event: HTMLElementEventMap[K] | SVGElementEventMap[K]) => void, element: RefObject, options?: boolean | AddEventListenerOptions, ): void; function useEventListener( eventName: string, handler: (event: Event) => void, element?: RefObject, options?: boolean | AddEventListenerOptions, ) { // Hold the handler in a ref so updating it never detaches the listener. const savedHandler = useRef(handler); useIsomorphicLayoutEffect(() => { savedHandler.current = handler; }, [handler]); useEffect(() => { const targetElement = element?.current ?? window; if (!targetElement?.addEventListener) return; const listener = (event: Event) => savedHandler.current(event); targetElement.addEventListener(eventName, listener, options); return () => { targetElement.removeEventListener(eventName, listener, options); }; }, [eventName, element, options]); } export { useEventListener }; ``` --- # useClickOutside > Runs a callback on outside click. - Category: DOM - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-click-outside ## Signature ```ts useClickOutside(ref: RefObject, callback: () => void): void ``` ## Parameters - `ref`: `RefObject` — Ref attached to the element that counts as inside — clicks within it (or its children) never fire the callback. - `callback`: `() => void` — Called on every mousedown outside the ref'd element — even while your UI is closed, so guard inside the callback if needed. ## Usage ```tsx import { useRef, useState } from "react"; import { useClickOutside } from "hookli"; export function Demo() { const menuRef = useRef(null); const [open, setOpen] = useState(false); useClickOutside(menuRef, () => setOpen(false)); return (
{open && (
  • Rename
  • Duplicate
)}
); } ``` ## Source `src/hooks/useClickOutside.hook.ts` ```ts import { Ref, useEffect } from "react"; type ClickOutsideCallback = () => void; export const useClickOutside = ( ref: Ref, callback: ClickOutsideCallback, ) => { useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( ref && "current" in ref && !ref.current?.contains(event.target as Node) ) { callback(); } }; document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, [ref, callback]); }; ``` --- # useMousePosition > Cursor coordinates within an element. - Category: DOM - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-mouse-position ## Signature ```ts useMousePosition(ref: RefObject): { x: number | null; y: number | null } ``` ## Parameters - `ref`: `RefObject` — Ref attached to the element the coordinates are measured against. ## Returns - `x`: `number | null` — Cursor X relative to the element's left edge; null until the first mousemove. Updates on every window mousemove, so it can go negative or exceed the element's width. - `y`: `number | null` — Cursor Y relative to the element's top edge; null until the first mousemove. ## Usage ```tsx import { useRef } from "react"; import { useMousePosition } from "hookli"; export function Demo() { const panelRef = useRef(null); const { x, y } = useMousePosition(panelRef); return (
{x === null || y === null ? (

Move your cursor

) : (

{Math.round(x)} × {Math.round(y)}

)}
); } ``` ## Source `src/hooks/useMousePosition.hook.ts` ```ts import { Ref, useEffect, useState } from "react"; interface MousePosition { x: number | null; y: number | null; } export const useMousePosition = ( ref: Ref, ): MousePosition => { const [mousePosition, setMousePosition] = useState({ x: null, y: null, }); useEffect(() => { const updateMousePosition = (event: MouseEvent) => { const { clientX, clientY } = event; if (ref && "current" in ref && ref.current) { const { left, top } = ref.current.getBoundingClientRect(); setMousePosition({ x: clientX - left, y: clientY - top }); } }; window.addEventListener("mousemove", updateMousePosition); return () => { window.removeEventListener("mousemove", updateMousePosition); }; }, [ref]); return mousePosition; }; ``` --- # useInfiniteScroll > Triggers loading near the scroll end. - Category: DOM - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-infinite-scroll ## Signature ```ts useInfiniteScroll(fetchMoreData: () => Promise): boolean ``` ## Parameters - `fetchMoreData`: `() => Promise` — Called when the window scroll comes within 500px of the document bottom. Must return a promise — the hook stays in the fetching state until it resolves. ## Returns - `isFetching`: `boolean` — True while a triggered fetchMoreData promise is pending; blocks re-triggering until it resolves. ## Usage ```tsx import { useCallback, useState } from "react"; import { useInfiniteScroll } from "hookli"; const page = (start: number) => Array.from({ length: 10 }, (_, i) => `Item ${start + i + 1}`); export function Demo() { const [items, setItems] = useState(() => page(0)); const fetchMoreData = useCallback( () => new Promise((resolve) => { setItems((prev) => [...prev, ...page(prev.length)]); resolve(); }), [], ); const isFetching = useInfiniteScroll(fetchMoreData); return (
    {items.map((item) => (
  • {item}
  • ))}
{isFetching &&

Loading more…

}
); } ``` ## Source `src/hooks/useInfiniteScroll.hook.ts` ```ts import { useEffect, useState } from "react"; type FetchMoreData = () => Promise; export const useInfiniteScroll = (fetchMoreData: FetchMoreData): boolean => { const [isFetching, setIsFetching] = useState(false); useEffect(() => { const handleScroll = () => { const isNearBottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 500; if (isNearBottom && !isFetching) { setIsFetching(true); fetchMoreData().then(() => setIsFetching(false)); } }; window.addEventListener("scroll", handleScroll); return () => { window.removeEventListener("scroll", handleScroll); }; }, [fetchMoreData, isFetching]); return isFetching; }; ``` --- # useExpandableText > Collapse long text by a character and/or line budget with a show-more toggle. - Category: DOM - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-expandable-text ## Signature ```ts useExpandableText(text: string, options?: UseExpandableTextOptions): UseExpandableTextResult ``` ## Parameters - `text`: `string` — The full text to (maybe) collapse. - `options`: `UseExpandableTextOptions` (default: `{}`) — Character and/or line budgets and display options. Whichever limit clips first wins. ## Returns - `text`: `string` — The text to render — character-capped when collapsed and maxChars is set, otherwise the full text. - `isExpanded`: `boolean` — Whether the full text is currently shown. - `isTruncated`: `boolean` — True when either limit actually clips the text — use it to hide the toggle when the text fits. - `toggle`: `() => void` — Flip between expanded and collapsed. - `expand`: `() => void` — Show the full text. - `collapse`: `() => void` — Collapse back to the limit. - `ref`: `RefCallback` — Attach to the text element. Required for the maxLines clamp and its overflow measurement. - `clampStyle`: `CSSProperties` — Spread onto the text element; applies the CSS line-clamp while collapsed and maxLines is set. ## Types ### UseExpandableTextOptions Character and line budgets — provide either, or both. - `maxChars`: `number` — Max characters shown while collapsed. Pure string logic (SSR-safe), trimmed to a word boundary. - `maxLines`: `number` — Max lines shown while collapsed. Applied as a CSS line-clamp and measured in the DOM, so it re-clips on resize. - `ellipsis`: `string` (default: `"…"`) — Appended to character-truncated text. - `defaultExpanded`: `boolean` (default: `false`) — Whether the text starts expanded. ## Usage ```tsx import { useExpandableText } from "hookli"; export function Review({ body }: { body: string }) { const { text, isExpanded, isTruncated, toggle, ref, clampStyle } = useExpandableText(body, { maxChars: 180, maxLines: 3 }); return (

{text}

{isTruncated && ( )}
); } ``` ## Source `src/hooks/use-expandable-text/use-expandable-text.ts` ```ts import { useCallback, useEffect, useState, type CSSProperties, type RefCallback, } from "react"; export interface UseExpandableTextOptions { maxChars?: number; maxLines?: number; ellipsis?: string; defaultExpanded?: boolean; } export interface UseExpandableTextResult { text: string; isExpanded: boolean; isTruncated: boolean; toggle: () => void; expand: () => void; collapse: () => void; ref: RefCallback; clampStyle: CSSProperties; } const truncateChars = ( text: string, maxChars: number, ellipsis: string, ): string => { if (text.length <= maxChars) return text; const slice = text.slice(0, maxChars); const lastSpace = slice.lastIndexOf(" "); const cut = lastSpace > 0 ? slice.slice(0, lastSpace) : slice; return cut.trimEnd() + ellipsis; }; export const useExpandableText = ( text: string, options: UseExpandableTextOptions = {}, ): UseExpandableTextResult => { const { maxChars, maxLines, ellipsis = "…", defaultExpanded = false } = options; const [isExpanded, setIsExpanded] = useState(defaultExpanded); const [node, setNode] = useState(null); const [lineOverflow, setLineOverflow] = useState(false); const charTruncated = maxChars !== undefined && text.length > maxChars; const collapsedText = charTruncated ? truncateChars(text, maxChars, ellipsis) : text; const displayText = isExpanded ? text : collapsedText; const ref = useCallback>((el) => setNode(el), []); // scrollHeight reflects the full content height regardless of the clamp, so // the overflow read is accurate in both states; the char cap alone drives // isTruncated when it has already shortened the string. useEffect(() => { if (!node || maxLines === undefined) { setLineOverflow(false); return; } const measure = () => { const style = window.getComputedStyle(node); let lineHeight = Number.parseFloat(style.lineHeight); if (!Number.isFinite(lineHeight)) { lineHeight = Number.parseFloat(style.fontSize) * 1.2; } const padding = Number.parseFloat(style.paddingTop) + Number.parseFloat(style.paddingBottom); const maxHeight = lineHeight * maxLines + (padding || 0); setLineOverflow(node.scrollHeight > maxHeight + 1); }; measure(); if (typeof ResizeObserver === "undefined") return; const observer = new ResizeObserver(measure); observer.observe(node); return () => observer.disconnect(); }, [node, maxLines, text, isExpanded]); const toggle = useCallback(() => setIsExpanded((value) => !value), []); const expand = useCallback(() => setIsExpanded(true), []); const collapse = useCallback(() => setIsExpanded(false), []); const clampStyle: CSSProperties = !isExpanded && maxLines !== undefined ? { display: "-webkit-box", WebkitBoxOrient: "vertical", WebkitLineClamp: maxLines, overflow: "hidden", } : {}; return { text: displayText, isExpanded, isTruncated: charTruncated || lineOverflow, toggle, expand, collapse, ref, clampStyle, }; }; ``` --- # useHover > Tracks whether the pointer is hovering an element. - Category: DOM - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-hover ## Signature ```ts useHover(elementRef: RefObject): boolean ``` ## Parameters - `elementRef`: `RefObject` — Ref to the element whose hover state to track. mouseenter/mouseleave are attached to it and cleaned up automatically. ## Returns - `isHovered`: `boolean` — True while the pointer is over the element, false otherwise. Starts false on the server and until the first mouseenter. ## Usage ```tsx import { useRef } from "react"; import { useHover } from "hookli"; export function Demo() { const boxRef = useRef(null); const isHovered = useHover(boxRef); return (
{isHovered ? "Pointer is over me" : "Hover this panel"}
); } ``` ## Source `src/hooks/use-hover/use-hover.ts` ```ts import { RefObject, useState } from "react"; import { useEventListener } from "../use-event-listener/use-event-listener"; export const useHover = ( elementRef: RefObject, ): boolean => { const [isHovered, setIsHovered] = useState(false); useEventListener("mouseenter", () => setIsHovered(true), elementRef); useEventListener("mouseleave", () => setIsHovered(false), elementRef); return isHovered; }; ``` --- # useIntersectionObserver > Observe an element's viewport intersection reactively. - Category: DOM - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-intersection-observer ## Signature ```ts useIntersectionObserver(options?: UseIntersectionObserverOptions): UseIntersectionObserverReturn ``` ## Parameters - `options`: `UseIntersectionObserverOptions` (default: `{}`) — Observer thresholds, root, and behaviour flags. All optional. ## Returns - `ref`: `(node: Element | null) => void` — Ref callback to attach to the element you want to observe. - `isIntersecting`: `boolean` — Whether the observed element currently intersects the root. - `entry`: `IntersectionObserverEntry | null` — The most recent observer entry (intersectionRatio, boundingClientRect…), or null before the first report. ## Types ### UseIntersectionObserverOptions Configures the underlying IntersectionObserver. - `threshold`: `number | number[]` (default: `0`) — One or more visibility ratios at which to fire. - `root`: `Element | Document | null` (default: `null`) — The element used as the viewport. Defaults to the browser viewport. - `rootMargin`: `string` (default: `"0%"`) — Margin around the root, in CSS-margin syntax — grows or shrinks the trigger area. - `freezeOnceVisible`: `boolean` (default: `false`) — Once the target is visible, stop observing and keep the visible state. - `initialIsIntersecting`: `boolean` (default: `false`) — isIntersecting value used before the observer first reports. - `onChange`: `(isIntersecting: boolean, entry: IntersectionObserverEntry) => void` — Called with the latest entry whenever intersection changes. ### UseIntersectionObserverReturn The ref callback plus the current intersection state. - `ref`: `(node: Element | null) => void` — Attach to the element you want to observe. - `isIntersecting`: `boolean` — Whether the target currently intersects the root. - `entry`: `IntersectionObserverEntry | null` — The most recent observer entry, or null before the first report. ## Usage ```tsx import { useIntersectionObserver } from "hookli"; export function Demo() { const { ref, isIntersecting } = useIntersectionObserver({ threshold: 0.5, }); return (
{isIntersecting ? "In view" : "Scroll me into view"}
); } ``` ## Source `src/hooks/use-intersection-observer/use-intersection-observer.ts` ```ts import { useCallback, useEffect, useRef, useState } from "react"; interface UseIntersectionObserverOptions { threshold?: number | number[]; root?: Element | Document | null; rootMargin?: string; freezeOnceVisible?: boolean; initialIsIntersecting?: boolean; onChange?: ( isIntersecting: boolean, entry: IntersectionObserverEntry, ) => void; } interface UseIntersectionObserverReturn { ref: (node: Element | null) => void; isIntersecting: boolean; entry: IntersectionObserverEntry | null; } export const useIntersectionObserver = ({ threshold = 0, root = null, rootMargin = "0%", freezeOnceVisible = false, initialIsIntersecting = false, onChange, }: UseIntersectionObserverOptions = {}): UseIntersectionObserverReturn => { const [element, setElement] = useState(null); const [isIntersecting, setIsIntersecting] = useState(initialIsIntersecting); const [entry, setEntry] = useState(null); const onChangeRef = useRef(onChange); onChangeRef.current = onChange; const frozen = entry?.isIntersecting && freezeOnceVisible; const ref = useCallback((node: Element | null) => { setElement(node); }, []); useEffect(() => { if (!element) return; if (frozen) return; if (typeof IntersectionObserver === "undefined") return; const observer = new IntersectionObserver( ([observerEntry]) => { setEntry(observerEntry); setIsIntersecting(observerEntry.isIntersecting); onChangeRef.current?.(observerEntry.isIntersecting, observerEntry); }, { threshold, root, rootMargin }, ); observer.observe(element); return () => { observer.disconnect(); }; }, [element, JSON.stringify(threshold), root, rootMargin, frozen]); return { ref, isIntersecting, entry }; }; ``` --- # useResizeObserver > Measure an element's size reactively via ResizeObserver. - Category: DOM - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-resize-observer ## Signature ```ts useResizeObserver(ref: RefObject, options?: UseResizeObserverOptions): ResizeObserverSize ``` ## Parameters - `ref`: `RefObject` — Ref to the element to measure. The observer attaches to ref.current. - `options`: `UseResizeObserverOptions` (default: `{}`) — Which box to measure and an optional resize callback. ## Returns - `width`: `number | undefined` — The element's measured width; undefined until the first observed layout. - `height`: `number | undefined` — The element's measured height; undefined until the first observed layout. ## Types ### UseResizeObserverOptions Configures the underlying ResizeObserver. - `box`: `ResizeObserverBoxOptions` (default: `"content-box"`) — Which box model to measure: content-box, border-box or device-pixel-content-box. - `onResize`: `(size: ResizeObserverSize) => void` — Called with the freshly measured size on every resize. ### ResizeObserverSize The size the hook returns; both values are undefined until the first measurement. - `width`: `number | undefined` — Measured width in pixels. - `height`: `number | undefined` — Measured height in pixels. ## Usage ```tsx import { useRef } from "react"; import { useResizeObserver } from "hookli"; export function Demo() { const boxRef = useRef(null); const { width, height } = useResizeObserver(boxRef); return (
{width === undefined ? "Measuring…" : `${Math.round(width)} × ${Math.round(height ?? 0)}`}
); } ``` ## Source `src/hooks/use-resize-observer/use-resize-observer.ts` ```ts import { RefObject, useEffect, useRef, useState } from "react"; interface ResizeObserverSize { width: number | undefined; height: number | undefined; } interface UseResizeObserverOptions { box?: ResizeObserverBoxOptions; onResize?: (size: ResizeObserverSize) => void; } export const useResizeObserver = ( ref: RefObject, options: UseResizeObserverOptions = {}, ): ResizeObserverSize => { const { box = "content-box" } = options; const [size, setSize] = useState({ width: undefined, height: undefined, }); const onResizeRef = useRef(options.onResize); onResizeRef.current = options.onResize; const previous = useRef({ width: undefined, height: undefined, }); useEffect(() => { const element = ref.current; if (!element) return; if (typeof ResizeObserver === "undefined") return; const observer = new ResizeObserver(([entry]) => { if (!entry) return; const boxSize = box === "border-box" ? entry.borderBoxSize : box === "device-pixel-content-box" ? entry.devicePixelContentBoxSize : entry.contentBoxSize; const measured = Array.isArray(boxSize) ? boxSize[0] : boxSize; const width = measured ? measured.inlineSize : entry.contentRect.width; const height = measured ? measured.blockSize : entry.contentRect.height; if ( previous.current.width === width && previous.current.height === height ) { return; } const next = { width, height }; previous.current = next; setSize(next); onResizeRef.current?.(next); }); observer.observe(element, { box }); return () => { observer.disconnect(); }; }, [ref, box]); return size; }; ``` --- # useScrollLock > Lock and restore scrolling on the body or an element. - Category: DOM - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-scroll-lock ## Signature ```ts useScrollLock(options?: UseScrollLockOptions): UseScrollLockReturn ``` ## Parameters - `options`: `UseScrollLockOptions` (default: `{}`) — Auto-lock behaviour, the lock target, and scrollbar compensation. ## Returns - `isLocked`: `boolean` — Whether the target's scroll is currently locked. - `lock`: `() => void` — Lock the target's scroll (sets overflow: hidden, optionally padding-compensated). - `unlock`: `() => void` — Restore the target's original scroll behaviour. ## Types ### UseScrollLockOptions Controls what is locked and when. - `autoLock`: `boolean` (default: `true`) — Lock automatically on mount and restore on unmount. - `lockTarget`: `HTMLElement | string` (default: ``) — Element (or CSS selector) whose scroll to lock. Defaults to the document body. - `widthReflow`: `boolean` (default: `true`) — Compensate for the removed scrollbar with padding so the layout does not shift. ### UseScrollLockReturn The current lock state plus manual controls. - `isLocked`: `boolean` — Whether the target's scroll is currently locked. - `lock`: `() => void` — Lock the target's scroll. - `unlock`: `() => void` — Restore the target's original scroll behaviour. ## Usage ```tsx import { useScrollLock } from "hookli"; export function Modal({ onClose }: { onClose: () => void }) { // Locks scroll on mount, restores it on unmount. useScrollLock(); return (
Scrolling behind this modal is frozen.
); } ``` ## Source `src/hooks/use-scroll-lock/use-scroll-lock.ts` ```ts import { useCallback, useRef, useState } from "react"; import { useIsomorphicLayoutEffect } from "../use-isomorphic-layout-effect/use-isomorphic-layout-effect"; interface UseScrollLockOptions { autoLock?: boolean; lockTarget?: HTMLElement | string; widthReflow?: boolean; } interface UseScrollLockReturn { isLocked: boolean; lock: () => void; unlock: () => void; } interface OriginalStyle { overflow: string; paddingRight: string; } export const useScrollLock = ( options: UseScrollLockOptions = {}, ): UseScrollLockReturn => { const { autoLock = true, lockTarget, widthReflow = true } = options; const [isLocked, setIsLocked] = useState(false); const target = useRef(null); const originalStyle = useRef(null); const resolveTarget = useCallback((): HTMLElement | null => { if (typeof document === "undefined") return null; if (lockTarget instanceof HTMLElement) return lockTarget; if (typeof lockTarget === "string") { return document.querySelector(lockTarget); } return document.body; }, [lockTarget]); const lock = useCallback(() => { const node = resolveTarget(); if (!node) return; target.current = node; originalStyle.current = { overflow: node.style.overflow, paddingRight: node.style.paddingRight, }; if (widthReflow && typeof window !== "undefined") { const scrollbarWidth = window.innerWidth - node.clientWidth; if (scrollbarWidth > 0) { const currentPadding = parseInt(window.getComputedStyle(node).paddingRight, 10) || 0; node.style.paddingRight = `${currentPadding + scrollbarWidth}px`; } } node.style.overflow = "hidden"; setIsLocked(true); }, [resolveTarget, widthReflow]); const unlock = useCallback(() => { const node = target.current; if (!node || !originalStyle.current) return; node.style.overflow = originalStyle.current.overflow; node.style.paddingRight = originalStyle.current.paddingRight; originalStyle.current = null; setIsLocked(false); }, []); useIsomorphicLayoutEffect(() => { if (!autoLock) return; lock(); return () => { unlock(); }; }, [autoLock, lock, unlock]); return { isLocked, lock, unlock }; }; ``` --- # useClickAnyWhere > Run a handler on every click anywhere in the document. - Category: DOM - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-click-any-where ## Signature ```ts useClickAnyWhere(handler: (event: MouseEvent) => void): void ``` ## Parameters - `handler`: `(event: MouseEvent) => void` — Called with the MouseEvent on every document-wide click. The latest handler is always used — no stale closure. ## Usage ```tsx import { useState } from "react"; import { useClickAnyWhere } from "hookli"; export function Demo() { const [clicks, setClicks] = useState(0); useClickAnyWhere(() => setClicks((prev) => prev + 1)); return

Document clicks: {clicks}

; } ``` ## Source `src/hooks/use-click-any-where/use-click-any-where.ts` ```ts import { useEventListener } from "../use-event-listener/use-event-listener"; export const useClickAnyWhere = ( handler: (event: MouseEvent) => void, ): void => { useEventListener("click", handler); }; ``` --- # useMediaQuery > Tracks whether a CSS media query currently matches. - Category: DOM - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-media-query ## Signature ```ts useMediaQuery(query: string, options?: UseMediaQueryOptions): boolean ``` ## Parameters - `query`: `string` — A CSS media query string, e.g. "(min-width: 768px)" or "(prefers-color-scheme: dark)". - `options`: `UseMediaQueryOptions` (default: `{}`) — SSR default and hydration behaviour. Optional. ## Returns - `matches`: `boolean` — Whether the query currently matches. Re-renders on every matchMedia change event, and starts from defaultValue on the server. ## Types ### UseMediaQueryOptions Configures the server value and mount behaviour. - `defaultValue`: `boolean` (default: `false`) — Value returned on the server and before hydration. - `initializeWithValue`: `boolean` (default: `true`) — Read the real match synchronously on mount. Set false to always start from defaultValue and avoid a hydration mismatch. ## Usage ```tsx import { useMediaQuery } from "hookli"; export function Demo() { const isWide = useMediaQuery("(min-width: 768px)"); return

{isWide ? "Desktop layout" : "Mobile layout"}

; } ``` ## Source `src/hooks/use-media-query/use-media-query.ts` ```ts import { useState } from "react"; import { useIsomorphicLayoutEffect } from "../use-isomorphic-layout-effect/use-isomorphic-layout-effect"; interface UseMediaQueryOptions { defaultValue?: boolean; initializeWithValue?: boolean; } const IS_SERVER = typeof window === "undefined"; export function useMediaQuery( query: string, options: UseMediaQueryOptions = {}, ): boolean { const { defaultValue = false, initializeWithValue = true } = options; const getMatches = (mediaQuery: string): boolean => { if (IS_SERVER) return defaultValue; return window.matchMedia(mediaQuery).matches; }; const [matches, setMatches] = useState(() => { if (initializeWithValue) return getMatches(query); return defaultValue; }); useIsomorphicLayoutEffect(() => { if (IS_SERVER) return; const matchMedia = window.matchMedia(query); const handleChange = () => setMatches(matchMedia.matches); handleChange(); matchMedia.addEventListener("change", handleChange); return () => { matchMedia.removeEventListener("change", handleChange); }; }, [query]); return matches; } ``` --- # useScreen > Tracks window.screen, refreshing it on every resize. - Category: DOM - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-screen ## Signature ```ts useScreen(options?: UseScreenOptions): Screen | null ``` ## Parameters - `options`: `UseScreenOptions` (default: `{}`) — Hydration behaviour. Optional. ## Returns - `screen`: `Screen | null` — The current window.screen (width, height, availWidth, colorDepth, orientation…), refreshed on every resize. null on the server and until hydration. ## Types ### UseScreenOptions Configures mount behaviour. - `initializeWithValue`: `boolean` (default: `true`) — Read the real screen synchronously on mount. Set false to start as null and populate after hydration. ## Usage ```tsx import { useScreen } from "hookli"; export function Demo() { const screen = useScreen(); return

Screen: {screen ? `${screen.width}×${screen.height}` : "…"}

; } ``` ## Source `src/hooks/use-screen/use-screen.ts` ```ts import { useState } from "react"; import { useEventListener } from "../use-event-listener/use-event-listener"; import { useIsomorphicLayoutEffect } from "../use-isomorphic-layout-effect/use-isomorphic-layout-effect"; interface UseScreenOptions { initializeWithValue?: boolean; } const IS_SERVER = typeof window === "undefined"; export function useScreen(options: UseScreenOptions = {}): Screen | null { const { initializeWithValue = true } = options; const readScreen = (): Screen | null => { if (IS_SERVER) return null; return window.screen; }; const [screen, setScreen] = useState(() => { if (initializeWithValue) return readScreen(); return null; }); const handleSize = () => { setScreen(readScreen()); }; useEventListener("resize", handleSize); useIsomorphicLayoutEffect(() => { handleSize(); }, []); return screen; } ``` --- # useWindowSize > Tracks the viewport's { width, height }, updated on resize. - Category: DOM - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-window-size ## Signature ```ts useWindowSize(options?: UseWindowSizeOptions): WindowSize ``` ## Parameters - `options`: `UseWindowSizeOptions` (default: `{}`) — Hydration behaviour. Optional. ## Returns - `width`: `number` — The viewport's inner width in pixels. 0 on the server and until hydration. - `height`: `number` — The viewport's inner height in pixels. 0 on the server and until hydration. ## Types ### WindowSize The viewport size reported by useWindowSize. - `width`: `number` — The viewport's inner width in pixels. - `height`: `number` — The viewport's inner height in pixels. ### UseWindowSizeOptions Configures mount behaviour. - `initializeWithValue`: `boolean` (default: `true`) — Read the real size synchronously on mount. Set false to start at 0 and populate after hydration. ## Usage ```tsx import { useWindowSize } from "hookli"; export function Demo() { const { width, height } = useWindowSize(); return

{width} × {height}

; } ``` ## Source `src/hooks/use-window-size/use-window-size.ts` ```ts import { useState } from "react"; import { useEventListener } from "../use-event-listener/use-event-listener"; import { useIsomorphicLayoutEffect } from "../use-isomorphic-layout-effect/use-isomorphic-layout-effect"; interface WindowSize { width: number; height: number; } interface UseWindowSizeOptions { initializeWithValue?: boolean; } const IS_SERVER = typeof window === "undefined"; export function useWindowSize(options: UseWindowSizeOptions = {}): WindowSize { const { initializeWithValue = true } = options; const readSize = (): WindowSize => ({ width: window.innerWidth, height: window.innerHeight, }); const [windowSize, setWindowSize] = useState(() => { if (initializeWithValue && !IS_SERVER) return readSize(); return { width: 0, height: 0 }; }); const handleSize = () => { if (IS_SERVER) return; setWindowSize(readSize()); }; useEventListener("resize", handleSize); useIsomorphicLayoutEffect(() => { handleSize(); }, []); return windowSize; } ``` --- # useCopyToClipboard > Copy text to the clipboard, tracking the last copied value. - Category: DOM - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-copy-to-clipboard ## Signature ```ts useCopyToClipboard(): [CopiedValue, CopyFn] ``` ## Returns - `[0] copiedText`: `CopiedValue` — The last successfully-copied string, or null before any copy or after a failed one. - `[1] copy`: `CopyFn` — Writes text to the clipboard. Resolves true on success, false when the API is unavailable or the write is rejected. ## Types ### CopiedValue - `value`: `string | null` — The tracked copied text, or null. ### CopyFn - `value`: `(text: string) => Promise` — Copies text and reports whether the write succeeded. ## Usage ```tsx import { useCopyToClipboard } from "hookli"; export function Demo() { const [copiedText, copy] = useCopyToClipboard(); return ( ); } ``` ## Source `src/hooks/use-copy-to-clipboard/use-copy-to-clipboard.ts` ```ts import { useCallback, useState } from "react"; type CopiedValue = string | null; type CopyFn = (text: string) => Promise; type UseCopyToClipboardReturn = [CopiedValue, CopyFn]; export function useCopyToClipboard(): UseCopyToClipboardReturn { const [copiedText, setCopiedText] = useState(null); const copy: CopyFn = useCallback(async (text) => { if (typeof navigator === "undefined" || !navigator.clipboard) { return false; } try { await navigator.clipboard.writeText(text); setCopiedText(text); return true; } catch { setCopiedText(null); return false; } }, []); return [copiedText, copy]; } ``` --- # useScript > Load an external script and report its load status. - Category: DOM - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-script ## Signature ```ts useScript(src: string | null, options?: UseScriptOptions): UseScriptStatus ``` ## Parameters - `src`: `string | null` — The script URL to load, or null to skip loading and stay idle. - `options`: `UseScriptOptions` (default: `{}`) — Optional shouldPreventLoad and removeOnUnmount flags. ## Returns - `status`: `UseScriptStatus` — The current load status: "idle" | "loading" | "ready" | "error". ## Types ### UseScriptStatus - `value`: `"idle" | "loading" | "ready" | "error"` — Lifecycle of the injected script tag. ### UseScriptOptions Flags controlling when the script loads and unloads. - `shouldPreventLoad`: `boolean` (default: `false`) — Keep the status "idle" without injecting the script — useful for deferring a load. - `removeOnUnmount`: `boolean` (default: `false`) — Remove the injected