10 Actually Useful React Hooks Patterns (2026)
In this tutorial, you'll learn about 10 actually useful react hooks patterns (2026). We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
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