Custom hooks let you extract and reuse stateful logic. Here are 10 essential hooks every React project needs.
1. useLocalStorage
Persist state in localStorage with type safety and SSR support.
"use client";
import { useState, useEffect, useCallback } from "react";
export function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(initialValue);
// Load from localStorage on mount
useEffect(() => {
try {
const item = window.localStorage.getItem(key);
if (item) setStoredValue(JSON.parse(item));
} catch {
console.warn(`Error reading localStorage key "${key}"`);
}
}, [key]);
const setValue = useCallback(
(value: T | ((val: T) => T)) => {
setStoredValue((prev) => {
const newValue = value instanceof Function ? value(prev) : value;
try {
window.localStorage.setItem(key, JSON.stringify(newValue));
} catch {
console.warn(`Error setting localStorage key "${key}"`);
}
return newValue;
});
},
[key]
);
const removeValue = useCallback(() => {
setStoredValue(initialValue);
window.localStorage.removeItem(key);
}, [key, initialValue]);
return [storedValue, setValue, removeValue] as const;
}
2. useDebounce
Debounce a value for search inputs and API calls.
"use client";
import { useState, useEffect } from "react";
export function useDebounce<T>(value: T, delay: number = 500): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
3. useClickOutside
Detect clicks outside a ref element.
"use client";
import { useEffect, useRef } from "react";
export function useClickOutside<T extends HTMLElement>(
handler: () => void
) {
const ref = useRef<T>(null);
useEffect(() => {
function handleClick(event: MouseEvent) {
if (ref.current && !ref.current.contains(event.target as Node)) {
handler();
}
}
document.addEventListener("mousedown", handleClick);
return () => document.removeEventListener("mousedown", handleClick);
}, [handler]);
return ref;
}
Usage:
function Dropdown() {
const [open, setOpen] = useState(false);
const ref = useClickOutside<HTMLDivElement>(() => setOpen(false));
return (
<div ref={ref}>
<button onClick={() => setOpen(!open)}>Toggle</button>
{open && <div className="dropdown-menu">...</div>}
</div>
);
}
4. useMediaQuery
Respond to CSS media queries in JavaScript.
"use client";
import { useState, useEffect } from "react";
export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false);
useEffect(() => {
const media = window.matchMedia(query);
setMatches(media.matches);
function listener(event: MediaQueryListEvent) {
setMatches(event.matches);
}
media.addEventListener("change", listener);
return () => media.removeEventListener("change", listener);
}, [query]);
return matches;
}
Usage:
function Layout() {
const isMobile = useMediaQuery("(max-width: 768px)");
const prefersDark = useMediaQuery("(prefers-color-scheme: dark)");
return isMobile ? <MobileNav /> : <DesktopNav />;
}
5. useCopyToClipboard
Copy text to clipboard with success feedback.
"use client";
import { useState, useCallback } from "react";
export function useCopyToClipboard(resetDelay: number = 2000) {
const [copied, setCopied] = useState(false);
const copy = useCallback(
async (text: string) => {
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), resetDelay);
return true;
} catch {
setCopied(false);
return false;
}
},
[resetDelay]
);
return { copied, copy };
}
6. useToggle
Simple boolean toggle with optional initial value.
"use client";
import { useState, useCallback } from "react";
export function useToggle(initialValue: boolean = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue((v) => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return { value, toggle, setTrue, setFalse, setValue };
}
7. useKeyPress
Detect specific keyboard key presses.
"use client";
import { useEffect, useCallback } from "react";
interface UseKeyPressOptions {
key: string;
ctrlKey?: boolean;
metaKey?: boolean;
shiftKey?: boolean;
preventDefault?: boolean;
}
export function useKeyPress(
options: UseKeyPressOptions | string,
handler: () => void
) {
const opts = typeof options === "string" ? { key: options } : options;
const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
if (event.key !== opts.key) return;
if (opts.ctrlKey && !event.ctrlKey) return;
if (opts.metaKey && !event.metaKey) return;
if (opts.shiftKey && !event.shiftKey) return;
if (opts.preventDefault) event.preventDefault();
handler();
},
[opts, handler]
);
useEffect(() => {
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [handleKeyDown]);
}
8. useIntersectionObserver
Detect when an element enters the viewport.
"use client";
import { useEffect, useRef, useState } from "react";
interface UseIntersectionOptions {
threshold?: number;
rootMargin?: string;
triggerOnce?: boolean;
}
export function useIntersectionObserver<T extends HTMLElement>({
threshold = 0.1,
rootMargin = "0px",
triggerOnce = false,
}: UseIntersectionOptions = {}) {
const ref = useRef<T>(null);
const [isIntersecting, setIsIntersecting] = useState(false);
useEffect(() => {
const element = ref.current;
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => {
setIsIntersecting(entry.isIntersecting);
if (entry.isIntersecting && triggerOnce) {
observer.unobserve(element);
}
},
{ threshold, rootMargin }
);
observer.observe(element);
return () => observer.disconnect();
}, [threshold, rootMargin, triggerOnce]);
return { ref, isIntersecting };
}
9. useCountdown
Countdown timer with pause, resume, and reset.
"use client";
import { useState, useEffect, useCallback, useRef } from "react";
export function useCountdown(initialSeconds: number) {
const [seconds, setSeconds] = useState(initialSeconds);
const [isRunning, setIsRunning] = useState(false);
const intervalRef = useRef<ReturnType<typeof setInterval>>(null);
useEffect(() => {
if (isRunning && seconds > 0) {
intervalRef.current = setInterval(() => {
setSeconds((s) => {
if (s <= 1) {
setIsRunning(false);
return 0;
}
return s - 1;
});
}, 1000);
}
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
}, [isRunning, seconds]);
const start = useCallback(() => setIsRunning(true), []);
const pause = useCallback(() => setIsRunning(false), []);
const reset = useCallback(() => {
setIsRunning(false);
setSeconds(initialSeconds);
}, [initialSeconds]);
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
const formatted = `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
return { seconds, formatted, isRunning, start, pause, reset, isFinished: seconds === 0 };
}
10. useFetch
Type-safe data fetching with loading and error states.
"use client";
import { useState, useEffect, useCallback } from "react";
interface UseFetchResult<T> {
data: T | null;
error: string | null;
isLoading: boolean;
refetch: () => void;
}
export function useFetch<T>(url: string, options?: RequestInit): UseFetchResult<T> {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const fetchData = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const response = await fetch(url, options);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const json = await response.json();
setData(json);
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error");
} finally {
setIsLoading(false);
}
}, [url, options]);
useEffect(() => {
fetchData();
}, [fetchData]);
return { data, error, isLoading, refetch: fetchData };
}
Summary
These 10 hooks cover the most common patterns:
| Hook | Purpose |
|---|---|
| useLocalStorage | Persist state to localStorage |
| useDebounce | Debounce rapidly changing values |
| useClickOutside | Close dropdowns/modals on outside click |
| useMediaQuery | Respond to screen size changes |
| useCopyToClipboard | Copy text with feedback |
| useToggle | Boolean state management |
| useKeyPress | Keyboard shortcut handlers |
| useIntersectionObserver | Viewport visibility detection |
| useCountdown | Timer with pause/resume |
| useFetch | Data fetching with states |
Need Custom React Development?
We build scalable React applications with clean, reusable architecture. Contact us to discuss your project.