10 Actually Useful React Hooks Patterns (2026)
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: '' });
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro