Every React project ends up with the same little hooks/ folder — a debounce, a localStorage wrapper, a click-outside. I got tired of re-copying them from old repos, so I put the eight I actually reuse into one place, each with a demo you can poke and the source one click away.
▶ Live demo: https://react-hooks-showcase.vercel.app/
Source (all in one file): https://github.com/dev48v/react-hooks-showcase/blob/main/src/hooks.ts
The eight: useDebounce, usePrevious, useLocalStorage, useMediaQuery, useOnClickOutside, useCopyToClipboard, useToggle, useIntersectionObserver. All dependency-free and typed. Here are the four with a subtlety worth knowing.
useDebounce — the cleanup is the whole trick
function useDebounce<T>(value: T, delay = 400): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const t = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(t); // ← reset the timer on every change
}, [value, delay]);
return debounced;
}
Each keystroke re-runs the effect, and the cleanup cancels the previous pending timeout. So the timer only fires once you stop typing. Fire your search/API call off the debounced value, not the raw one, and you go from one request per keystroke to one per pause.
usePrevious — why it returns last render's value
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => { ref.current = value; }, [value]);
return ref.current;
}
It looks like it should return the current value, but it doesn't — because effects run after render. During this render ref.current still holds what the last effect wrote (the previous value); then the effect updates it for next time. That one-render lag is exactly what you want for "did this prop just change?" logic.
useMediaQuery — don't listen to resize
function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(() => window.matchMedia(query).matches);
useEffect(() => {
const mq = window.matchMedia(query);
const onChange = () => setMatches(mq.matches);
mq.addEventListener("change", onChange); // ← not window.resize
setMatches(mq.matches);
return () => mq.removeEventListener("change", onChange);
}, [query]);
return matches;
}
The common mistake is a window.resize listener that re-renders on every pixel. matchMedia fires its change event only when the query result actually flips — one re-render at the breakpoint, not hundreds while dragging.
useOnClickOutside — every dropdown needs it
function useOnClickOutside(ref, handler) {
useEffect(() => {
const listener = (e) => {
if (!ref.current || ref.current.contains(e.target)) return;
handler(e); // click was outside → fire
};
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => { /* remove both */ };
}, [ref, handler]);
}
Attach it to your menu's ref and close on outside click. Use mousedown (not click) so it fires before a re-render can move the target out from under the event, and add touchstart for mobile.
The remaining four — useLocalStorage (lazy init + error-swallowing writes), useCopyToClipboard, useToggle (memoized toggler), and useIntersectionObserver (lazy-load / infinite-scroll without scroll math) — are in the demo, each interactive with its source a click away.
Grab whichever you need; that's what they're for. If the collection saved you a trip to an old repo, a star helps others find it: https://github.com/dev48v/react-hooks-showcase
Top comments (0)