usehooks-ts
usehooks-ts is a typed collection of React hooks for browser state and common component behavior. Its 34 exports cover local and session storage, media queries, element and window sizing, event listeners, outside clicks, clipboard access, timers, debouncing, dark mode, script loading, and small state helpers. It is not a state manager or component kit; each named export wraps a focused React or Web Platform pattern and the package is marked side-effect-free for tree shaking.
A good middle-sized hook collection when a project needs several browser utilities and will treat SSR options deliberately. Do not add it for one five-line hook, and do not mistake it for a data-fetching or state-management layer.
Use it if
- You need several ordinary browser hooks and want consistent TypeScript signatures, cleanup, and tests instead of maintaining copies
- Your app supports React 16.8 through 19 and needs one package whose peer range explicitly covers all of them
- You use server rendering and value the documented initializeWithValue switches on storage, media, screen, and window hooks
- You want ESM and CommonJS entry points with declarations included and named exports that bundlers can tree-shake
- You need only one trivial hook: a local useEffect or useState wrapper is easier to audit and avoids depending on the release schedule of a 34-hook collection
- You are building React Server Components without client boundaries: these hooks use React state, effects, refs, and browser APIs, so consuming components must run on the client
- You expect server rendering to be automatic: browser-reading hooks default initializeWithValue to true, and the declarations tell SSR users to set it false to avoid initial client-only reads and hydration surprises
- You need async data fetching, request caching, forms, or global application state: v3 removed the previously deprecated useFetch and intentionally stays focused on browser and component utilities
- You want a dependency-free debounce hook: lodash.debounce is the package's one runtime dependency and backs its debounce behavior
Setup reality
Install usehooks-ts alongside React; version 3.1.1 declares React ^16.8, 17, 18, 19, or the 19 release candidate as a peer and requires Node 16.15 or newer for its toolchain compatibility. It includes TypeScript declarations, ESM import and CommonJS require targets, is marked sideEffects: false, and has one runtime dependency, lodash.debounce. There is no provider or config file. The work is at component boundaries. Every consumer is a client component in frameworks such as Next.js App Router, so add the client directive at the top of the importing file. Browser-backed hooks often read on first render by default. For SSR, pass initializeWithValue: false to useLocalStorage, useSessionStorage, useReadLocalStorage, useMediaQuery, useWindowSize, useScreen, useDarkMode, or useTernaryDarkMode, then accept that the server's fallback may change after hydration. Storage values are JSON by default; version 2.11 removed automatic Date, Map, and Set support, so provide matching serializer and deserializer functions for those values. Clipboard access needs a secure context and user permission. IntersectionObserver and ResizeObserver need browser support or test mocks, and jsdom does not implement their layout behavior. useOnClickOutside expects refs to mounted nodes, while useEventListener's target is a RefObject, not ref.current. Version 3 removed deprecated hooks and signatures, so check the changelog before copying v2 examples from blogs.
Patterns
Store typed state in localStoragepersist-local-state
import { useLocalStorage } from 'usehooks-ts';
const [filters, setFilters, removeFilters] = useLocalStorage(
'product-filters',
{ query: '', inStock: false },
);
setFilters(current => ({ ...current, inStock: true }));The default serializer is JSON-based. removeFilters() deletes the key and was added in v3.1.
Delay localStorage reads during SSRhydrate-storage-safely
const [theme, setTheme] = useLocalStorage(
'theme',
'system',
{ initializeWithValue: false },
);The server and first client render use the initial value; the stored value is read after mounting, so the UI may update once.
Persist a value with custom serializationserialize-date
const [lastSeen, setLastSeen] = useLocalStorage<Date>(
'last-seen',
new Date(0),
{
serializer: value => value.toISOString(),
deserializer: raw => new Date(raw),
},
);v3 does not automatically restore Date, Map, or Set instances; serializer and deserializer must agree.
Track a responsive media querymatch-media-query
import { useMediaQuery } from 'usehooks-ts';
const isNarrow = useMediaQuery('(max-width: 48rem)', {
defaultValue: false,
initializeWithValue: false,
});Disabling the initial browser read avoids server hydration mismatch, but responsive content changes after mount.
Close a dialog on outside interactionhandle-outside-click
import { useRef } from 'react';
import { useOnClickOutside } from 'usehooks-ts';
const dialogRef = useRef<HTMLDivElement>(null);
useOnClickOutside(dialogRef, () => setOpen(false), 'mousedown');
return <div ref={dialogRef}>Dialog content</div>;The default event is mousedown. The hook also accepts an array of refs and touch or focus event types.
Attach a typed window event listenerlisten-window-event
import { useEventListener } from 'usehooks-ts';
useEventListener('keydown', event => {
if (event.key === 'Escape') setOpen(false);
});Omit the element argument for window events; cleanup and the latest handler reference are managed by the hook.
Listen on a referenced elementlisten-element-event
const buttonRef = useRef<HTMLButtonElement>(null);
useEventListener('focus', () => setFocused(true), buttonRef);
return <button ref={buttonRef}>Save</button>;Pass the RefObject itself, not buttonRef.current, so the hook can follow the mounted target.
Debounce a search callbackdebounce-search
import { useDebounceCallback } from 'usehooks-ts';
const search = useDebounceCallback(
(query: string) => runSearch(query),
300,
{ trailing: true, maxWait: 1200 },
);
return <input onChange={event => search(event.target.value)} />;The returned function also exposes cancel(), flush(), and isPending(); calls can return undefined before the first invocation.
Load content when it first becomes visibleobserve-intersection
import { useIntersectionObserver } from 'usehooks-ts';
const { ref, isIntersecting } = useIntersectionObserver({
threshold: 0.25,
rootMargin: '200px',
freezeOnceVisible: true,
});
return <section ref={ref}>{isIntersecting ? <Results /> : null}</section>;freezeOnceVisible stops observer updates after the first intersection; tests need an IntersectionObserver mock.
Track an element with ResizeObserverobserve-element-size
const panelRef = useRef<HTMLDivElement>(null);
const { width = 0, height = 0 } = useResizeObserver({
ref: panelRef,
box: 'border-box',
});
return <div ref={panelRef}>{width} x {height}</div>;Passing onResize avoids re-rendering on each size change; ResizeObserver support or a polyfill is required.
Pause and resume an intervalrun-interval
import { useInterval } from 'usehooks-ts';
useInterval(
() => setSeconds(value => value + 1),
running ? 1000 : null,
);A null delay clears the interval without changing hook call order; the callback always sees current React state when using a functional update.
Copy text and report successcopy-clipboard
import { useCopyToClipboard } from 'usehooks-ts';
const [copiedText, copy] = useCopyToClipboard();
async function copyLink() {
const ok = await copy(location.href);
if (!ok) setError('Copy failed');
}Clipboard writes usually require HTTPS or localhost and a user gesture; the function resolves false rather than throwing the browser error to your component.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| react-use | npm | Choose it when breadth matters more and you need a much larger catalog including sensors, lifecycle, and async utilities |
| ahooks | npm | Choose it for enterprise React apps that want richer request, polling, DOM, and state hooks in one library |
| @uidotdev/usehooks | npm | Choose it when you prefer a smaller modern hook set paired with tutorial-style examples |