Skip to content
10 Actually Useful React Hooks Patterns (2026)

10 Actually Useful React Hooks Patterns (2026)

DodaTech Updated Jun 20, 2026 4 min read

Every React tutorial covers useState and useEffect. Those are the building blocks, not the destination. This list contains custom hooks that solve recurring problems — debouncing input, detecting clicks outside elements, persisting to localStorage, and managing form state. Each one eliminates a pattern you’d otherwise repeat across every project.

The Hooks

useDebounce — Delays updating a value until after a specified delay of no changes. Essential for search inputs that trigger API calls.

function useDebounce<T>(value: T, delay: number): T {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);
  return debounced;
}
// Usage: const debouncedQuery = useDebounce(searchTerm, 300);

usePrevious — Stores the previous value of a state or prop. Useful for detecting changes in useEffect dependencies.

function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T>();
  useEffect(() => { ref.current = value; });
  return ref.current;
}
// Usage: const prevCount = usePrevious(count);

useOnClickOutside — Triggers a callback when a click occurs outside a referenced element. The standard pattern for dropdowns, modals, and popovers.

function useOnClickOutside(ref: RefObject<HTMLElement>, handler: () => void) {
  useEffect(() => {
    const listener = (e: MouseEvent | TouchEvent) => {
      if (!ref.current || ref.current.contains(e.target as Node)) return;
      handler();
    };
    document.addEventListener('mousedown', listener);
    return () => document.removeEventListener('mousedown', listener);
  }, [ref, handler]);
}
// Usage: useOnClickOutside(menuRef, () => setIsOpen(false));

useIntersectionObserver — Detects when an element becomes visible in the viewport. Powers infinite scroll, lazy loading images, and analytics tracking.

function useIntersectionObserver(ref: RefObject<HTMLElement>, options?: IntersectionObserverInit) {
  const [isIntersecting, setIsIntersecting] = useState(false);
  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      setIsIntersecting(entry.isIntersecting);
    }, options);
    if (ref.current) observer.observe(ref.current);
    return () => observer.disconnect();
  }, [ref, options]);
  return isIntersecting;
}
// Usage: const isVisible = useIntersectionObserver(sentinelRef);

useLocalStorage — Syncs a state variable with localStorage, surviving page refreshes.

function useLocalStorage<T>(key: string, initial: T): [T, (v: T) => void] {
  const [value, setValue] = useState<T>(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initial;
  });
  useEffect(() => localStorage.setItem(key, JSON.stringify(value)), [key, value]);
  return [value, setValue];
}
// Usage: const [theme, setTheme] = useLocalStorage('theme', 'dark');

useMediaQuery — Tracks whether a CSS media query matches. Enables responsive logic in JavaScript without window resize listeners.

function useMediaQuery(query: string): boolean {
  const [matches, setMatches] = useState(() => window.matchMedia(query).matches);
  useEffect(() => {
    const mql = window.matchMedia(query);
    const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
    mql.addEventListener('change', handler);
    return () => mql.removeEventListener('change', handler);
  }, [query]);
  return matches;
}
// Usage: const isMobile = useMediaQuery('(max-width: 768px)');

useInterval — A declarative setInterval that cleans up on unmount and respects changing dependencies.

function useInterval(callback: () => void, delay: number | null) {
  const saved = useRef(callback);
  useEffect(() => { saved.current = callback; });
  useEffect(() => {
    if (delay === null) return;
    const id = setInterval(() => saved.current(), delay);
    return () => clearInterval(id);
  }, [delay]);
}
// Usage: useInterval(() => setCount(c => c + 1), 1000);

useFetch — Wraps fetch with loading, error, and data states. Handles race conditions by ignoring stale responses.

function useFetch<T>(url: string): { data: T | null; loading: boolean; error: Error | null } {
  const [state, setState] = useState({ data: null, loading: true, error: null });
  useEffect(() => {
    let cancelled = false;
    fetch(url).then(r => r.json()).then(data => {
      if (!cancelled) setState({ data, loading: false, error: null });
    }).catch(error => {
      if (!cancelled) setState({ data: null, loading: false, error });
    });
    return () => { cancelled = true; };
  }, [url]);
  return state;
}
// Usage: const { data, loading } = useFetch<User[]>('/api/users');

useToggle — A boolean state with a built-in toggle function. Eliminates writing setIsOpen(!isOpen) dozens of times.

function useToggle(initial = false): [boolean, () => void] {
  const [value, setValue] = useState(initial);
  const toggle = useCallback(() => setValue(v => !v), []);
  return [value, toggle];
}
// Usage: const [isOpen, toggleOpen] = useToggle();

useForm — Lightweight form state management with validation. A simpler alternative to Formik or React Hook Form for small forms.

function useForm<T extends Record<string, any>>(initial: T) {
  const [values, setValues] = useState(initial);
  const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
  const handleChange = useCallback((field: keyof T) => (e: React.ChangeEvent<HTMLInputElement>) => {
    setValues(v => ({ ...v, [field]: e.target.value }));
    setErrors(e => ({ ...e, [field]: undefined }));
  }, []);
  return { values, errors, setErrors, handleChange, setValues };
}
// Usage: const { values, handleChange } = useForm({ email: '', password: '' });
Which hook should I build first?
useDebounce. It prevents the most common performance mistake: firing API calls on every keystroke. Combine it with useFetch for a search-as-you-type component in about 20 lines.
Should I use these or a library like React Hook Form?
For small projects (under 5 forms), custom hooks keep your bundle lean and give you full control. For large forms with complex validation, use React Hook Form or Formik — they handle touched-state, async validation, and field arrays out of the box.
Are these production-safe?
Yes. Each hook handles cleanup (returning from useEffect), respects React’s rules of hooks, and avoids stale closures by using refs where needed. Test them with React Strict Mode to verify double-invocation safety.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro