Taming DOM Events in React: useEventListener, useEventEmitter, useKeyModifier, useTextSelection, useDebounceFn, useThrottleFn

The DOM event model and the React render model do not get along. addEventListener wants a stable function reference; React hands you a new closure on every render. setTimeout-backed debounces want to outlive a frame; React reaches in and unmounts the component while the timer is still running. The keyboard tells you a key went down with one event and back up with another, but if the user alt-tabs in between, the up event never arrives and your "Shift is held" flag is stuck on true forever. The Selection API does not even fire selectionchange reliably on the same Selection object — it mutates the existing one and expects you to notice.

Every codebase ends up with the same patches for these. A useEffect that adds and removes a listener. A lodash debounce inside a ref. A keydown/keyup reducer with an Alt+Tab workaround that nobody quite remembers writing. The patches work. They are also five lines of intent buried under twenty lines of cleanup, and the cleanup is exactly where the bugs live.

ReactUse ships six small event hooks that fold the cleanup into the hook itself. This post walks each one: the bug in the naive version, what the hook does instead, and a component you would actually write with it. If you read the post on the ref escape hatch, the pattern will be familiar — every hook in this list closes over its callback through useLatest so the listener stays stable even as the function identity changes.