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