Custom hooks are just functions whose names start with use and that call other hooks. The prefix is not cosmetic — React's linter and DevTools use it to recognize hook calls and surface their state. The real reason to extract a custom hook is reuse: any time two components want the same combination of state, effect, and cleanup, lifting it into a hook keeps both call sites in sync. A tiny useToggle, a slightly larger useCounter, and a more substantial useFormStatus all share the same shape: state plus bound actions returned from one function. Common reusable hooks include useDebounced, useThrottle, useLocalStorage, useWindowSize, useMediaQuery, useFetch, useScript, useOnClickOutside, and useKeyboard.
The two trickiest families are debouncing and data fetching. A useDebounced(value, ms) hook keeps an internal state, sets a timeout when the value changes, and clears the timeout in cleanup, so the returned value lags behind by ms after the input stops changing. A throttled hook uses a ref to track the last timestamp and either ignores intermediate calls or schedules a trailing one. For fetching, the canonical skeleton uses an "off" flag in the effect: declare let off = false, set it true in cleanup, and guard setData with !off so a slow response can't setState after unmount. In modern code, an AbortController passed as signal to fetch does the same job more explicitly, and a ref-tracked request id lets you ignore responses that have been superseded by a newer request. Combining both gives you a hook that survives StrictMode double-mounts and rapidly changing URLs without leaking listeners.
A few cross-cutting rules apply to nearly every custom hook. Cleanup is non-negotiable: if your hook subscribes, listens, or schedules a timer, the effect must return a function that undoes it; otherwise StrictMode's double-mount will double-fire and unmounted components will leak. For SSR safety, initialize from localStorage lazily inside useState and persist in an effect that only runs after mount; reading browser-only APIs during render causes hydration mismatches. UI-bound hooks follow the same recipe: useScript appends a tag and tracks loaded/error state with cleanup; useOnClickOutside listens on document for mousedown and fires when the target is outside the ref; useHover wires onMouseEnter and onMouseLeave; useMediaQuery listens to matchMedia's change event; a viewport scroll hook uses a requestAnimationFrame ref to throttle. Some side effects belong in custom hooks too: setting document.title on a prop change with cleanup that restores the previous title; sending analytics on route change; or implementing form draft autosave with a debounced effect. Across all of these, the same warning holds: if your hook is not synchronizing with an external system, you probably don't need an effect at all.