React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

best-practicessummaryreact-events

React Events Best Practices

A condensed summary of the 25 most important best practices drawn from every page in this section.

  1. preventDefault Before setData on Copy: Calling 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).
  2. Make Non-input Paste Targets Focusable: <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.
  3. preventDefault on dragover to Opt Into Drop: An element is not a valid drop target until onDragOver calls e.preventDefault(); without it the cursor shows "not allowed" and onDrop never fires, which is the single most common HTML5 drag bug.
  4. Read dataTransfer Only at dragstart/drop: For security, 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.
  5. Don't Use Native DnD on Mobile: HTML5 drag-and-drop is effectively broken on mobile browsers, so switch to a library like dnd-kit or build custom pointer-event logic when touch support matters.
  6. Detect Real Focus Exit With contains: Use 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.
  7. React Focus Events Bubble: Unlike native 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.
  8. Use onMouseDown for Click-After-Blur: When a dropdown dismisses on 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.
  9. onChange Fires Every Keystroke: React's 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.
  10. File Inputs Are Always Uncontrolled: Browsers forbid setting value on <input type="file">, so read files through onChangeonChange={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 = "".
  11. Don't Mix action With onSubmit: React 19's form 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.
  12. Use onKeyDown, Not onKeyPress: onKeyPress is deprecated and never fires for Escape, arrows, Tab, or modifiers; use onKeyDown for every keyboard interaction including shortcuts and accessible widget navigation.
  13. e.key vs e.code: 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.
  14. Guard Against e.repeat: 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.
  15. Filter onTransitionEnd by propertyName: 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() }}.
  16. Don't Use onTimeUpdate for Frame-Accurate UI: Browsers throttle 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.
  17. onMouseEnter/Leave Don't Bubble: 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.
  18. Throttle onMouseMove With rAF: 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.
  19. Call setPointerCapture Synchronously: To keep a drag alive when the pointer leaves the element at high velocity, call it synchronously in onPointerDownonPointerDown={e => e.currentTarget.setPointerCapture(e.pointerId)} — calling it later or from an effect is too late to redirect subsequent events.
  20. Set touch-action: none for Pointer Drag: Passive-default touch scrolling still fights your drag unless you disable it in CSS; add touch-action: none on the draggable element so the browser yields the gesture to your pointer handlers.
  21. onPointerLeave Still Fires During Capture: Active pointer capture does not suppress onPointerLeave, so use onGotPointerCapture/onLostPointerCapture to track real capture state instead of inferring it from enter/leave events.
  22. Page Scroll Needs a window Listener: onScroll on the root or body only fires when that element itself has a scrollbar, so use useEffectuseEffect(() => { const h = () => {}; window.addEventListener("scroll", h, { passive: true }); return () => window.removeEventListener("scroll", h); }, []) — and always pass { passive: true } to keep compositor-thread scrolling fast.
  23. Prefer IntersectionObserver Over onScroll: 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.
  24. Use changedTouches in onTouchEnd: In 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.
  25. React Touch Listeners Are Passive: React 17+ registers touch handlers as passive at the delegation root, so 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.