@chalidade/react-hooks
Small, tested, SSR-safe React hooks. Each one solves a problem that's easy to get wrong by hand: stale closures, listeners that are never removed, or hydration mismatches.
pnpm add @chalidade/react-hooksNot published yet
This package isn't on npm yet. For now, copy the source from GitHub.
Requires React 18 or newer (useSyncExternalStore).
Overview
| Hook | Use it for |
|---|---|
useDebouncedValue | A value that waits until input stops (search) |
useDebouncedCallback | A debounced function that's stable across renders (autosave) |
useThrottledCallback | At most once per interval (scroll, resize, drag) |
useOnline | Connection status for an offline banner |
useMediaQuery | Responsive logic in JS without hydration mismatches |
useLocalStorage | State that persists and syncs across tabs |
useEventListener | Listeners that clean up after themselves |
useOnClickOutside | Closing a dropdown/popover on outside click |
useIntersectionObserver | Lazy loading, infinite scroll, enter animations |
usePrevious | The value from the previous render |
useLatest | A ref holding the latest value (no stale closures) |
useIsomorphicLayoutEffect | useLayoutEffect without server warnings |
useDebouncedValue
const [input, setInput] = useState("");
const query = useDebouncedValue(input, 300);
useEffect(() => {
if (query) void search(query);
}, [query]);useDebouncedCallback
The function identity stays the same across renders but always calls the latest callback. A pending call is cancelled on unmount, so there's no setState after unmount.
const save = useDebouncedCallback((draft: string) => api.save(draft), 1000);
<textarea onChange={(e) => save(e.target.value)} onBlur={() => save.flush()} />useThrottledCallback
The first call runs immediately; later calls in the same window are merged into one call at the end of the window with the latest arguments. Good for events that fire continuously during an interaction.
const onScroll = useThrottledCallback(() => setProgress(readScrollProgress()), 100);
useEventListener(window, "scroll", onScroll, { passive: true });useOnline
const online = useOnline();
return online ? null : <p role="status">You are offline. Changes are saved locally.</p>;useMediaQuery
const isMobile = useMediaQuery("(max-width: 640px)");
const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)");On the server it returns false (or the second argument). Because it uses useSyncExternalStore, React uses the server value during hydration and then updates, so there's no hydration mismatch.
useLocalStorage
const [theme, setTheme, resetTheme] = useLocalStorage<"light" | "dark">("theme", "light");- Other components using the same key update too.
- Other tabs update via the
storageevent. - Corrupt data or blocked storage (private mode) won't crash the app: the value falls back to
initialValue.
useEventListener
useEventListener(window, "keydown", (e) => {
if (e.key === "Escape") close();
});
const ref = useRef<HTMLDivElement>(null);
useEventListener(ref, "scroll", onScroll, { passive: true });The event type follows the target (KeyboardEvent for keydown, and so on).
useOnClickOutside
const ref = useRef<HTMLDivElement>(null);
useOnClickOutside(ref, () => setOpen(false));
return <div ref={ref}>{open && <Menu />}</div>;Uses pointerdown so it also works on touch screens.
useIntersectionObserver
const { ref, isIntersecting } = useIntersectionObserver({ rootMargin: "200px", once: true });
return <img ref={ref} src={isIntersecting ? src : undefined} alt={alt} />;usePrevious
const prevCount = usePrevious(count);
const direction = prevCount === undefined ? null : count > prevCount ? "up" : "down";useLatest
const onTickRef = useLatest(onTick);
useEffect(() => {
const id = setInterval(() => onTickRef.current(), 1000);
return () => clearInterval(id);
}, []); // the interval doesn't need re-registering when onTick changesLearn from the source
Try writing your own useLocalStorage with useState + useEffect, then run the tests in packages/react-hooks. Which cases fail? (Hint: two components sharing a key, and the first render on the server.)
Related concepts: 07 Framework, 17 Browser APIs, 18 Rendering Strategies.