React Custom Hooks
React custom hooks cheat sheet with patterns for state management, data fetching, debounce, and reusable logic with code examples.
Other React Sheets
Sign in to mark items as known and track your progress.
Sign inCustom Hook Fundamentals
Learn the fundamentals of creating reusable custom hooks, from basic patterns to advanced composition techniques
Creating Custom Hooks
Master the art of creating reusable custom hooks for state management, side effects, and logic encapsulation
// Basic Custom Hook Pattern
import { useState, useEffect } from 'react'
// useCounter - Simple state management hook
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue)
const increment = () => setCount(prev => prev + 1)
const decrement = () => setCount(prev => prev - 1)
const reset = () => setCount(initialValue)
return { count, increment, decrement, reset }
}
// Using the hook
function Counter() {
const { count, increment, decrement, reset } = useCounter(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
)
}// Complete Counter Component
function CounterApp() {
const counter1 = useCounter(0)
const counter2 = useCounter(100)
return (
<div>
<div>
<h3>Counter 1: {counter1.count}</h3>
<button onClick={counter1.increment}>+</button>
<button onClick={counter1.decrement}>-</button>
<button onClick={counter1.reset}>Reset</button>
</div>
<div>
<h3>Counter 2: {counter2.count}</h3>
<button onClick={counter2.increment}>+</button>
<button onClick={counter2.decrement}>-</button>
<button onClick={counter2.reset}>Reset</button>
</div>
</div>
)
}Advanced Hook Patterns
Explore advanced patterns including hook composition, factory hooks, and complex state management
// Hook with cleanup
function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
// Hook with dependencies
function useFetch(url, options) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const abortController = new AbortController();
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url, {
...options,
signal: abortController.signal
});
const result = await response.json();
setData(result);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err);
}
} finally {
setLoading(false);
}
};
fetchData();
return () => abortController.abort();
}, [url, JSON.stringify(options)]);
return { data, loading, error };
}Data Fetching Hooks
Build robust data fetching hooks with loading states, error handling, caching, and request cancellation
API Integration Hooks
Build production-ready data fetching hooks with caching, error handling, retries, and optimistic updates
// Basic fetch hook
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
const result = await response.json();
setData(result);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}Utility & Performance Hooks
Create utility hooks for common tasks like local storage, debouncing, and performance optimization
Performance Optimization Hooks
Optimize React app performance with debouncing, throttling, memoization, and lazy loading hooks
// Memoization hook
function useMemoizedValue(fn, dependencies) {
return useMemo(fn, dependencies);
}
// Callback memoization
function useStableCallback(callback, dependencies) {
return useCallback(callback, dependencies);
}
// Ref for DOM access
function useElementRef() {
const ref = useRef(null);
const focus = () => ref.current?.focus();
const blur = () => ref.current?.blur();
const scrollIntoView = () => ref.current?.scrollIntoView();
return { ref, focus, blur, scrollIntoView };
}Browser & Storage Hooks
Sync React state with localStorage, media queries, and theme
useLocalStorage
State that persists to localStorage with cross-tab sync
import { useState, useEffect, useCallback } from 'react'
function useLocalStorage<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
if (typeof window === 'undefined') return initial
try {
const raw = localStorage.getItem(key)
return raw ? (JSON.parse(raw) as T) : initial
} catch {
return initial
}
})
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value))
} catch {}
}, [key, value])
return [value, setValue] as const
}
// Usage
const [theme, setTheme] = useLocalStorage('theme', 'light')useMediaQuery
Subscribe to a CSS media query - responsive logic in JS
import { useSyncExternalStore } from 'react'
function useMediaQuery(query: string) {
return useSyncExternalStore(
(cb) => {
const mql = window.matchMedia(query)
mql.addEventListener('change', cb)
return () => mql.removeEventListener('change', cb)
},
() => window.matchMedia(query).matches,
() => false, // SSR fallback
)
}
// Usage
const isMobile = useMediaQuery('(max-width: 768px)')
const prefersDark = useMediaQuery('(prefers-color-scheme: dark)')
const reducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)')Interaction & Lifecycle Hooks
Practical hooks for debounce, click-outside, event listeners, and prior values
useDebouncedValue & useDebouncedCallback
Debounce a changing value or a callback - search inputs, autosave
import { useEffect, useState, useRef, useCallback } from 'react'
function useDebouncedValue<T>(value: T, delay = 300) {
const [debounced, setDebounced] = useState(value)
useEffect(() => {
const t = setTimeout(() => setDebounced(value), delay)
return () => clearTimeout(t)
}, [value, delay])
return debounced
}
// Usage
function Search() {
const [q, setQ] = useState('')
const debouncedQ = useDebouncedValue(q, 300)
useEffect(() => {
if (debouncedQ) fetch('/api/search?q=' + debouncedQ)
}, [debouncedQ])
return <input value={q} onChange={(e) => setQ(e.target.value)} />
}useClickOutside & useEventListener
Dismiss menus/popovers and attach typed event listeners safely
import { useEffect, useRef, RefObject } from 'react'
function useClickOutside<T extends HTMLElement>(
ref: RefObject<T>,
onOutside: (e: MouseEvent | TouchEvent) => void,
) {
useEffect(() => {
const handler = (e: MouseEvent | TouchEvent) => {
const el = ref.current
if (!el || el.contains(e.target as Node)) return
onOutside(e)
}
document.addEventListener('mousedown', handler)
document.addEventListener('touchstart', handler)
return () => {
document.removeEventListener('mousedown', handler)
document.removeEventListener('touchstart', handler)
}
}, [ref, onOutside])
}
// Usage
function Dropdown() {
const ref = useRef<HTMLDivElement>(null)
const [open, setOpen] = useState(false)
useClickOutside(ref, () => setOpen(false))
return <div ref={ref}>{open && <Menu />}</div>
}usePrevious & useIsFirstRender
Track the previous value of a variable, detect the first render
import { useRef, useEffect } from 'react'
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined)
useEffect(() => { ref.current = value }, [value])
return ref.current
}
// Usage
function Counter({ count }: { count: number }) {
const prev = usePrevious(count)
return <p>Now: {count}, was: {prev ?? '-'}</p>
}useIntersectionObserver
Detect when an element enters the viewport - lazy load, infinite scroll
import { useEffect, useRef, useState } from 'react'
function useIntersectionObserver(options?: IntersectionObserverInit) {
const ref = useRef<HTMLElement>(null)
const [entry, setEntry] = useState<IntersectionObserverEntry>()
useEffect(() => {
const el = ref.current
if (!el) return
const io = new IntersectionObserver(([e]) => setEntry(e), options)
io.observe(el)
return () => io.disconnect()
}, [options?.root, options?.rootMargin, options?.threshold])
return { ref, entry, isIntersecting: !!entry?.isIntersecting }
}
// Usage
function LazyImage({ src, alt }: { src: string; alt: string }) {
const { ref, isIntersecting } = useIntersectionObserver({ rootMargin: '200px' })
return (
<div ref={ref as any}>
{isIntersecting ? <img src={src} alt={alt} /> : <div className="placeholder" />}
</div>
)
}