React comes with built-in hooks like useState
, useEffect
, and useRef
, but sometimes you need small utilities that keep your code clean and readable. Here are a couple of custom hooks I personally use all the time in my projects.
🧱 useHasMounted
This hook returns true
only after the component has mounted on the client. It's especially useful when dealing with SSR (e.g., Next.js) and avoiding hydration mismatches.
"use client";
import { useEffect, useState } from "react";
export const useHasMounted = () => {
const [hasMounted, setHasMounted] = useState(false);
useEffect(() => {
setHasMounted(true);
}, []);
return hasMounted;
};
🖱️ useHovering
Cross-device hover detection — works for mouse and touch. Instead of juggling multiple handlers, this hook gives you a unified boolean state and handler set.
"use client";
import { useState } from "react";
export const useHovering = () => {
const [hovering, setHovering] = useState(false);
const handleTouchStart = () => setHovering(true);
const handleTouchEnd = () => setHovering(false);
const handleMouseEnter = () => setHovering(true);
const handleMouseLeave = () => setHovering(false);
return {
hovering,
handleTouchStart,
handleTouchEnd,
handleMouseEnter,
handleMouseLeave,
};
};
🖥️ useMediaQuery
This hook lets you listen to CSS media queries in React. Use it to toggle components or styles based on viewport width, orientation, or any other media feature.
import { useState, useEffect } from "react";
export const useMediaQuery = (query: string) => {
const [matches, setMatches] = useState(false);
useEffect(() => {
const media = window.matchMedia(query);
setMatches(media.matches);
const listener = () => setMatches(media.matches);
media.addEventListener("change", listener);
return () => media.removeEventListener("change", listener);
}, [query]);
return matches;
};
🌐 useOutsideClick
Detect clicks outside a given element — perfect for closing dropdowns, modals, or tooltips when the user clicks elsewhere on the page.
import { useEffect, RefObject } from "react";
export const useOutsideClick = (ref: RefObject<HTMLElement>, handler: () => void) => {
useEffect(() => {
const listener = (e: MouseEvent) => {
if (!ref.current || ref.current.contains(e.target as Node)) return;
handler();
};
document.addEventListener("mousedown", listener);
return () => document.removeEventListener("mousedown", listener);
}, [ref, handler]);
};
⏳ useDebounce
Debounce a changing value — useful for search inputs, resize events, or any rapid-fire updates where you want to wait until the user pauses.
import { useState, useEffect } from "react";
export const useDebounce = <T>(value: T, delay = 300): T => {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
};
🔄 usePrevious
Keep track of the previous value of a prop or state. This is handy when you need to compare current vs. previous without external refs.
import { useRef, useEffect } from "react";
export const usePrevious = <T>(value: T): T | undefined => {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
};
🔒 useLockBodyScroll
Lock the <body>
scroll when a modal or sidebar is open—prevents background scrolling on mobile and desktop alike.
import { useLayoutEffect } from "react";
export const useLockBodyScroll = () => {
useLayoutEffect(() => {
const original = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = original;
};
}, []);
};
📱 useIsMobile
A handy hook to detect if the viewport width is below a given breakpoint (1024px by default). Perfect for responsive logic in your components.
import { useState, useEffect } from "react";
const MOBILE_BREAKPOINT = 1024;
export const useIsMobile = () => {
const [isMobile, setIsMobile] = useState(
typeof window !== "undefined" ? window.innerWidth < MOBILE_BREAKPOINT : false,
);
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
if (typeof window !== "undefined") {
handleResize();
window.addEventListener("resize", handleResize);
}
return () => window.removeEventListener("resize", handleResize);
}, []);
return isMobile;
};