React Cheatsheet
Custom Hooks
Use this React reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
What Custom Hooks Are
A custom hook is a JavaScript function whose name starts with use and that calls other hooks. They extract reusable stateful logic from components.
- Must follow the same rules of hooks as built-in hooks.
- Can call
useState,useEffect,useRef,useCallback, etc. - Can call other custom hooks.
- Each component instance that calls a hook gets its own isolated state.
// Minimal custom hook function useCounter(initial = 0) { const [count, setCount] = useState(initial); const increment = () => setCount(c => c + 1); const decrement = () => setCount(c => c - 1); const reset = () => setCount(initial); return { count, increment, decrement, reset }; } // Usage — each component gets its own count function Counter() { const { count, increment, reset } = useCounter(10); return <><p>{count}</p><button onClick={increment}>+</button><button onClick={reset}>Reset</button></>; }
Pattern: Encapsulating useEffect Logic
// useFetch — data fetching with loading/error state function useFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { if (!url) return; const controller = new AbortController(); setLoading(true); setError(null); fetch(url, { signal: controller.signal }) .then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }) .then(setData) .catch(err => { if (err.name !== 'AbortError') setError(err.message); }) .finally(() => setLoading(false)); return () => controller.abort(); }, [url]); return { data, loading, error }; } // Usage function UserProfile({ id }) { const { data: user, loading, error } = useFetch(`/api/users/${id}`); if (loading) return <Spinner />; if (error) return <p>Error: {error}</p>; return <h1>{user?.name}</h1>; }
Pattern: Local Storage State
function useLocalStorage(key, initialValue) { const [stored, setStored] = useState(() => { try { const item = localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch { return initialValue; } }); const setValue = useCallback((value) => { try { const v = value instanceof Function ? value(stored) : value; setStored(v); localStorage.setItem(key, JSON.stringify(v)); } catch (e) { console.error(e); } }, [key, stored]); return [stored, setValue]; } // Usage const [theme, setTheme] = useLocalStorage('theme', 'light');
Pattern: Previous Value
function usePrevious(value) { const ref = useRef(undefined); useEffect(() => { ref.current = value; }); return ref.current; // value from last render } function Tracker({ count }) { const prev = usePrevious(count); return <p>Changed from {prev} to {count}</p>; }
Pattern: Window Size / Media Query
function useWindowSize() { const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight, }); useEffect(() => { const handler = () => setSize({ width: window.innerWidth, height: window.innerHeight }); window.addEventListener('resize', handler); return () => window.removeEventListener('resize', handler); }, []); return size; } function useMediaQuery(query) { const [matches, setMatches] = useState(() => matchMedia(query).matches); useEffect(() => { const mql = matchMedia(query); const handler = (e) => setMatches(e.matches); mql.addEventListener('change', handler); return () => mql.removeEventListener('change', handler); }, [query]); return matches; } // Usage const isMobile = useMediaQuery('(max-width: 768px)');
Pattern: Online Status
function useOnlineStatus() { const [online, setOnline] = useState(navigator.onLine); useEffect(() => { const on = () => setOnline(true); const off = () => setOnline(false); window.addEventListener('online', on); window.addEventListener('offline', off); return () => { window.removeEventListener('online', on); window.removeEventListener('offline', off); }; }, []); return online; }
Pattern: Debounced Value
function useDebounce(value, delay = 300) { const [debounced, setDebounced] = useState(value); useEffect(() => { const id = setTimeout(() => setDebounced(value), delay); return () => clearTimeout(id); }, [value, delay]); return debounced; } // Usage — only search after user stops typing for 300ms function Search() { const [query, setQuery] = useState(''); const debouncedQuery = useDebounce(query, 300); const { data } = useFetch(`/api/search?q=${debouncedQuery}`); return <input value={query} onChange={e => setQuery(e.target.value)} />; }
Pattern: Throttled Callback
function useThrottle(fn, delay) { const lastRan = useRef(Date.now()); return useCallback((...args) => { if (Date.now() - lastRan.current >= delay) { fn(...args); lastRan.current = Date.now(); } }, [fn, delay]); }
Pattern: Toggle
function useToggle(initial = false) { const [value, setValue] = useState(initial); const toggle = useCallback(() => setValue(v => !v), []); const setOn = useCallback(() => setValue(true), []); const setOff = useCallback(() => setValue(false), []); return [value, toggle, setOn, setOff]; } const [open, toggleOpen, openDrawer, closeDrawer] = useToggle();
Pattern: Form State
function useForm(initial) { const [values, setValues] = useState(initial); const [errors, setErrors] = useState({}); const [touched, setTouched] = useState({}); const handleChange = (e) => { const { name, value, type, checked } = e.target; setValues(v => ({ ...v, [name]: type === 'checkbox' ? checked : value })); }; const handleBlur = (e) => { const { name } = e.target; setTouched(t => ({ ...t, [name]: true })); }; const reset = () => { setValues(initial); setErrors({}); setTouched({}); }; return { values, errors, touched, setErrors, handleChange, handleBlur, reset }; }
Pattern: Intersection Observer (Element Visibility)
function useInView(options = {}) { const ref = useRef(null); const [inView, setInView] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; const obs = new IntersectionObserver( ([entry]) => setInView(entry.isIntersecting), options ); obs.observe(el); return () => obs.disconnect(); }, [options.threshold, options.root, options.rootMargin]); return [ref, inView]; } // Usage function LazyImage({ src }) { const [ref, inView] = useInView({ threshold: 0.1 }); return <img ref={ref} src={inView ? src : undefined} alt="" />; }
Pattern: Clipboard
function useClipboard(timeout = 2000) { const [copied, setCopied] = useState(false); const copy = useCallback(async (text) => { await navigator.clipboard.writeText(text); setCopied(true); setTimeout(() => setCopied(false), timeout); }, [timeout]); return { copy, copied }; } function CopyButton({ text }) { const { copy, copied } = useClipboard(); return <button onClick={() => copy(text)}>{copied ? 'Copied!' : 'Copy'}</button>; }
Pattern: Event Listener
function useEventListener(target, type, handler, options) { const savedHandler = useRef(handler); useEffect(() => { savedHandler.current = handler; }, [handler]); useEffect(() => { const el = typeof target === 'function' ? target() : (target?.current ?? window); if (!el?.addEventListener) return; const listener = (e) => savedHandler.current(e); el.addEventListener(type, listener, options); return () => el.removeEventListener(type, listener, options); }, [target, type, options]); } // Usage useEventListener(null, 'keydown', e => e.key === 'Escape' && closeModal()); useEventListener(buttonRef, 'click', handleClick);
useDebugValue in Custom Hooks
import { useDebugValue } from 'react'; function useAuth() { const [user, setUser] = useState(null); useDebugValue(user ? `Logged in as ${user.name}` : 'Not logged in'); return { user, setUser }; } // Shows label in React DevTools → Hook inspector
Composing Custom Hooks
Custom hooks can call other custom hooks freely.
function useUserData(userId) { const { data: user, loading } = useFetch(`/api/users/${userId}`); const isOnline = useOnlineStatus(); const prevUserId = usePrevious(userId); const { copy } = useClipboard(); return { user, loading, isOnline, prevUserId, copyEmail: () => copy(user?.email) }; }
Rules Recap
// Hook names MUST start with "use" function useMyHook() { ... } // valid function myHook() { ... } // NOT a hook — linter won't enforce rules // Only call hooks at the top level (not inside conditions/loops) function useConditional(flag) { // WRONG: if (flag) { const [x] = useState(0); } // CORRECT: const [x] = useState(0); if (flag) { /* use x */ } } // Share logic, not state — each call gets isolated state const a = useCounter(); // independent const b = useCounter(); // independent