React Events Best Practices
A condensed summary of the 25 most important best practices drawn from every page in this section.
Search across all documentation pages
A condensed summary of the 25 most important best practices drawn from every page in this section.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
e.clipboardData.setData(...) in onCopy without e.preventDefault() lets the browser silently overwrite your custom payload with the selection; always prevent default first, then write every format you want (e.g., text/plain and text/html).<div> and other non-input elements never receive onPaste unless they hold focus, so add tabIndex={0} (or -1) plus an outline-safe focus style to make custom paste zones actually work.onDragOver calls e.preventDefault(); without it the cursor shows "not allowed" and onDrop never fires, which is the single most common HTML5 drag bug.dataTransfer.getData() returns "" during dragover/dragenter; only dataTransfer.types is inspectable during the drag, so feature-gate on types and read the payload in onDrop.dnd-kit or build custom pointer-event logic when touch support matters.e.currentTarget.contains(e.relatedTarget as Node) to tell "focus moved between children" from "focus truly left the container" - onBlur={e => { if (!e.currentTarget.contains(e.relatedTarget)) closeDropdown() }} - remember relatedTarget is null when focus leaves the document entirely.focus/blur, React's onFocus and onBlur are wired to focusin/focusout and bubble up the tree - so a parent sees every child focus change, and mixing React handlers with addEventListener("focus", …) yields inconsistent behavior.onBlur before a click on its option registers, swap the option's handler to onMouseDown + e.preventDefault() so the click is captured before focus moves.onChange maps to the native input event, not native change, so it fires on every keystroke instead of on blur; use onBlur (or useDeferredValue) when you genuinely want debounced commit semantics.value on <input type="file">, so read files through onChange - onChange={e => setFile(e.target.files?.[0])} - and a ref, and never try to control the value; clearing means assigning "" to the DOM node directly via inputRef.current!.value = "".action prop plus useActionState bypasses onSubmit entirely (no preventDefault needed); combining the two on the same form produces confusing double-handling, so pick one model per form.onKeyPress is deprecated and never fires for Escape, arrows, Tab, or modifiers; use onKeyDown for every keyboard interaction including shortcuts and accessible widget navigation.e.key is layout- and modifier-aware ("A" with Shift) and is what you want for shortcuts; e.code is the physical position ("KeyA") and is what you want for WASD-style games where layout should not matter.onKeyDown fires continuously while a key is held with e.repeat === true, so early-return on repeat - onKeyDown={e => { if (e.repeat) return; sendMessage() }} - for expensive handlers like network calls or state transitions you only want once per press.onTransitionEnd fires once per animated CSS property, so a transition on both opacity and transform dispatches twice; guard handlers: onTransitionEnd={e => { if (e.propertyName !== "opacity") return; onFadeComplete() }}.onTimeUpdate to roughly four times per second, which is too coarse for progress scrubs or waveform highlights - drive those from requestAnimationFrame reading video.currentTime instead.onMouseEnter/onMouseLeave fire only on the exact element, while onMouseOver/onMouseOut bubble and re-fire on every child crossing; mixing them causes flicker on nested hover regions, so pick enter/leave for hover UIs.onMouseMove fires on every pixel of movement and can trigger re-render storms; write to a ref and schedule updates with requestAnimationFrame, or accept that raw state updates will tank frame rate.onPointerDown - onPointerDown={e => e.currentTarget.setPointerCapture(e.pointerId)} - calling it later or from an effect is too late to redirect subsequent events.touch-action: none on the draggable element so the browser yields the gesture to your pointer handlers.onPointerLeave, so use onGotPointerCapture/onLostPointerCapture to track real capture state instead of inferring it from enter/leave events.onScroll on the root or body only fires when that element itself has a scrollbar, so use useEffect - useEffect(() => { const h = () => {}; window.addEventListener("scroll", h, { passive: true }); return () => window.removeEventListener("scroll", h); }, []) - and always pass { passive: true } to keep compositor-thread scrolling fast.IntersectionObserver runs off the main thread and is much cheaper than computing visibility from onScroll math; use it for lazy-load, infinite scroll, and "scrolled past header" style logic, and guard with isIntersecting since it fires immediately on observe.onTouchEnd the touches and targetTouches lists are empty (the finger that triggered the event has already lifted), so always read e.changedTouches[0] to get the final position and convert with Array.from(e.changedTouches) since TouchList is not an array.e.preventDefault() inside onTouchMove silently fails; either attach a non-passive listener via ref.addEventListener("touchmove", h, { passive: false }) or apply touch-action: none and let CSS block the gesture.Reviewed by Chris St. John·Last updated Jul 16, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥