# 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 (
{on ? "On" : "Off"}
setOn(true)}>Set on
setOn(false)}>Set off
);
}
```
## 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 (
);
}
```
## 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)} />
setStoredValue("")}>Clear
);
}
```
## 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 (
setStoredValue("hello")}>Save for 10s
{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)} />
Clear
);
}
```
## 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 (
{isDarkMode ? "Switch to light" : "Switch to dark"}
);
}
```
## 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 (
setTernaryDarkMode(e.target.value)}
>
Light
System
Dark
);
}
```
## 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"}
Toggle
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}
-1
+1
setCount(10)}>Set 10
Reset
);
}
```
## 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
Back
Next
Reset
);
}
```
## 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}
Start
Pause
Reset
);
}
```
## 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 (
set("lang", "en")}>Set lang
remove("theme")}>Remove theme
Reset
{[...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 (
);
}
```
## 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}
setRunning((prev) => !prev)}>
{running ? "Pause" : "Resume"}
);
}
```
## 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 (
setDelay(2000)}>Reveal in 2s
{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 (
setCount((prev) => prev + 1)}>{count}
alert(readLatest())}>Read latest
);
}
```
## 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 Load ;
}
```
## 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 (
setOpen((prev) => !prev)}>Actions
{open && (
)}
);
}
```
## 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 && (
{isExpanded ? "Show less" : "Show more"}
)}
);
}
```
## 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 (
copy("npm i hookli")}>
{copiedText ? "Copied!" : "Copy"}
);
}
```
## 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