Useful React Hooks I Actually Use

A personal collection of lightweight, battle-tested React hooks I use in almost every project — for mount detection, hover handling, and more.

May 23, 2025 (22d ago)

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;
};
Gradient background