special-hooks
v2.0.1
Published
A lightweight, tree-shakeable, zero-dependency collection of essential React hooks written in TypeScript.
Maintainers
Readme
special-hooks
A lightweight, tree-shakeable, zero-dependency collection of essential React hooks, written in TypeScript.
- Zero runtime dependencies - only
reactas a peer dependency. - Tree-shakeable - import 50 hooks or one; your bundle only ships what you use (
useToggleadds ~300 B). - TypeScript-first - every hook is fully typed with exported types.
- SSR-safe - browser APIs are guarded, so it works with Next.js, Remix, etc.
- Dual ESM + CJS builds with per-hook subpath exports.
Installation
npm install special-hooks
# or
yarn add special-hooks
# or
pnpm add special-hooksreact and react-dom (v18+) are peer dependencies.
Usage
Import from the package root:
import { useToggle, useDebounce, useLocalStorage } from "special-hooks";Or import a single hook via its subpath (handy for the smallest possible footprint):
import useToggle from "special-hooks/use-toggle";Hooks
State
useCounter
Numeric counter with clamping, custom step, set and reset.
const { count, increment, decrement, set, reset } = useCounter(0, {
min: 0,
max: 10,
step: 2,
});useToggle
Boolean state with stable helper actions.
const [isOpen, { toggle, setTrue, setFalse, set }] = useToggle(false);usePrevious
Returns the value from the previous render.
const previousCount = usePrevious(count);useStep
Navigate a multi-step flow (wizards, onboarding).
const [step, { goToNextStep, goToPrevStep, reset, canGoToNextStep }] = useStep(4);useDebounce
Debounce a changing value.
const debouncedSearch = useDebounce(search, 300);useThrottle
Throttle a changing value.
const throttledScroll = useThrottle(scrollY, 200);useDebouncedCallback
Debounce a function, with cancel and flush.
const save = useDebouncedCallback((value: string) => api.save(value), 500);
save("draft"); // runs 500ms after the last callStorage
useLocalStorage
Persist state to localStorage, with cross-tab sync and a remove action.
const { value, setValue, remove } = useLocalStorage("theme", "light");
setValue((prev) => (prev === "light" ? "dark" : "light"));useSessionStorage
Same API as useLocalStorage, backed by sessionStorage.
const { value, setValue, remove } = useSessionStorage("draft", "");Data
useFetch
Fetch JSON with abort-on-unmount, refetch, and request options.
const { data, loading, error, refetch } = useFetch<User[]>("/api/users", {
enabled: true,
});useAsync
Run an async function and track its lifecycle.
const { status, value, error, loading, execute } = useAsync(fetchProfile);useTable
Client-side pagination and sorting for arrays of data.
const {
paginatedData,
currentPage,
totalPages,
nextPage,
prevPage,
toggleSort,
sortBy,
sortDirection,
} = useTable({ data: rows, initialPageSize: 10 });useValidateEmail
Synchronous email validity check.
const isValid = useValidateEmail(email);UI / DOM
useClickOutside
Run a handler when the user clicks/taps outside the referenced element.
const ref = useClickOutside<HTMLDivElement>(() => setOpen(false));
return <div ref={ref}>...</div>;useHover
Track hover state of an element.
const [ref, isHovered] = useHover<HTMLButtonElement>();useMediaQuery
Reactively match a media query (SSR-safe).
const isDesktop = useMediaQuery("(min-width: 1024px)");useWindowSize
Reactive { width, height } of the window.
const { width, height } = useWindowSize();useElementSize / useResizeObserver
Track an element's size via ResizeObserver.
const [ref, { width, height }] = useElementSize<HTMLDivElement>();useIntersectionObserver
Observe element visibility for lazy-loading, infinite scroll, or reveal animations.
const { ref, isIntersecting } = useIntersectionObserver({ threshold: 0.5 });useScrollLock
Lock and unlock body scroll (modals, drawers) without layout shift.
const { lock, unlock } = useScrollLock();useAutoScroll
Keep a container pinned to the bottom as content arrives (chat-style), but only while the user is already at the bottom.
const { ref, isPinned, scrollToBottom } = useAutoScroll<HTMLDivElement>();useHorizontalOverflow / useVerticalOverflow
Detect whether a container's content overflows.
const ref = useRef<HTMLDivElement>(null);
const hasOverflow = useHorizontalOverflow(ref);useTheme
Light/dark theme with system-preference detection, persistence, and DOM application.
const { theme, setTheme, toggleTheme } = useTheme({ attribute: "data-theme" });useDocumentTitle
Set document.title, optionally restoring it on unmount.
useDocumentTitle("Dashboard");useCopyToClipboard
Copy text with success feedback and error reporting.
const { copied, error, copy } = useCopyToClipboard();
<button onClick={() => copy("hello")}>{copied ? "Copied!" : "Copy"}</button>;Lifecycle
useInterval
Declarative setInterval; pass null to pause.
useInterval(() => tick(), isRunning ? 1000 : null);useTimeout
Declarative setTimeout with reset/clear.
const { reset, clear } = useTimeout(() => setVisible(false), 3000);useUpdateEffect
Like useEffect, but skips the initial mount.
useUpdateEffect(() => {
console.log("value changed", value);
}, [value]);useIsMounted
Returns a function that reports whether the component is still mounted.
const isMounted = useIsMounted();
if (isMounted()) setState(data);useEventListener
Type-safe event listener for window, document, or an element ref.
useEventListener("keydown", (e) => {
if (e.key === "Escape") close();
});Network
useNetworkState / useOnline
Track connectivity and (where supported) connection details.
const { online, effectiveType, downlink } = useNetworkState();
const isOnline = useOnline();Migration to 2.0
2.0 is a ground-up rewrite. Notable breaking changes:
- Removed hooks:
useRandomEmails,useRandomColors,useDateTime, anduseCardTypewere removed (low utility / buggy). useCopyToClipboardnow returns{ copied, error, copy }(was[copied, handleCopy]) and no longer depends oncopy-to-clipboard.useLocalStorage/useSessionStoragenow return{ value, setValue, remove }(was[value, setValue]).useThemenow returns{ theme, setTheme, toggleTheme }(addedsetTheme) and accepts an options object.useFetchnow returns{ data, loading, error, refetch }and accepts request options.- The package is now zero-dependency, tree-shakeable, and ships dual ESM/CJS with per-hook subpath exports.
Development
npm install
npm test # run the test suite
npm run typecheck # type-check without emitting
npm run build # build ESM + CJS + type declarations
npm run size # check per-hook bundle sizesContributing
Contributions are welcome. Each hook lives in its own folder under src/hooks/<category>/<hook>/ with a co-located test. Add new hooks there, export them from src/index.ts, and include a subpath entry in package.json's exports.
License
Apache-2.0
