React Event Handling
Handle user events in React with camelCase handlers, synthetic events, and controlled inputs.
TL;DR
- 01Use camelCase event names like onClick and onChange in JSX.
- 02Pass a function reference as the handler, not a function call.
- 03Use e.preventDefault() and e.stopPropagation() to control event flow.
Tips
- 01Always use controlled components for forms — it gives you better control over input values and validation.
Warnings
- 01Remember to call preventDefault() on forms to prevent page reload on submission.
Basic Event Handlers
- Attach events with camelCase attribute names like
onClick, not lowercaseonclick.function Button() { function handleClick() { console.log("Button clicked"); } return <button onClick={handleClick}>Click me</button>; } - Pass a function reference, not a function call, or it runs on every render.
// Good: pass the function <button onClick={handleClick}>Click</button> // Bad: calls function immediately on render <button onClick={handleClick()}>Click</button> - Write inline handlers as arrow functions for short, one-off logic.
<button onClick={() => console.log("clicked")}>Click</button> - Pass extra arguments to a handler by wrapping the call in an arrow function.
function List({ items }) { return ( <ul> {items.map(item => ( <li key={item.id}> <button onClick={() => handleDelete(item.id)}> Delete {item.name} </button> </li> ))} </ul> ); } - Name handler functions with a
handleprefix so their purpose is obvious at a glance.function handleDelete(id) { /* ... */ } function handleToggle() { /* ... */ }
Common Event Handlers
- Handle form submission with
onSubmiton the<form>element, notonClickon the button.function LoginForm() { const handleSubmit = (e) => { e.preventDefault(); console.log("Form submitted"); }; return <form onSubmit={handleSubmit}></form>; } - Handle input changes with
onChange, reading the new value frome.target.value.function NameInput() { const [name, setName] = useState(""); return <input value={name} onChange={(e) => setName(e.target.value)} />; } - Handle mouse events like
onMouseEnter,onMouseLeave, andonMouseMovefor hover effects.<div onMouseEnter={() => setHovered(true)}> Hover me </div> - Handle keyboard shortcuts with
onKeyDown, checkinge.keyfor the pressed key.function SearchBox() { const handleKeyDown = (e) => { if (e.key === "Enter") submitSearch(); if (e.key === "Escape") clearSearch(); }; return <input onKeyDown={handleKeyDown} placeholder="Search..." />; } - Handle focus and blur events with
onFocusandonBlurfor accessibility cues.function Input() { const [focused, setFocused] = useState(false); return ( <input className={focused ? "focused" : ""} onFocus={() => setFocused(true)} onBlur={() => setFocused(false)} /> ); }
Event Object
- Access event properties like target and preventDefault.
function handleClick(event) { console.log(event.target); // the element clicked console.log(event.type); // "click" } function handleSubmit(event) { event.preventDefault(); // stop form submission event.stopPropagation(); // stop event bubbling } - React's SyntheticEvent wraps the browser's native event for cross-browser consistency; access the underlying event with
e.nativeEvent.function handleClick(e) { console.log(e.nativeEvent); // the underlying browser Event } - As of React 17, synthetic events are no longer pooled and reused: each event object is freshly created and stays valid for as long as you hold a reference to it, even after the handler returns. Calling
e.persist()is now a no-op kept only for backward compatibility — you never need to call it.// React 16 and earlier nulled out event fields after the handler ran, // so reading e.target.value async required e.persist() first. // React 17+: the event object is never pooled, so this just works. function handleChange(e) { setTimeout(() => console.log(e.target.value), 1000); } - Get input values with e.target.value.
const handleChange = (e) => { setName(e.target.value); }; - Read checkbox state with e.target.checked.
function Checkbox() { const [checked, setChecked] = useState(false); return ( <input type="checkbox" checked={checked} onChange={(e) => setChecked(e.target.checked)} /> ); } - Get the key pressed from keyboard events with e.key.
function handleKeyDown(e) { console.log(e.key); // "Enter", "Escape", "ArrowUp" console.log(e.code); // "Enter", "Escape", "ArrowUp" console.log(e.ctrlKey); // true if Ctrl held } - Access the current target when events bubble up.
function handleClick(e) { console.log(e.target); // element that was clicked console.log(e.currentTarget); // element with the handler }
Controlled vs Uncontrolled Components
- Controlled components: React manages the input value.
function TextInput() { const [value, setValue] = useState(""); return ( <input value={value} onChange={(e) => setValue(e.target.value)} /> ); } - Uncontrolled components: DOM manages the input.
function TextInput() { const inputRef = useRef(); function handleSubmit() { console.log(inputRef.current.value); } return ( <> <input ref={inputRef} /> <button onClick={handleSubmit}>Submit</button> </> ); } - Use defaultValue for uncontrolled inputs with an initial value.
// defaultValue sets the initial value but doesn't control updates <input defaultValue="Alice" ref={inputRef} /> - Controlled inputs enable real-time validation and formatting.
function PhoneInput() { const [phone, setPhone] = useState(""); const handleChange = (e) => { // Format as user types: 555-123-4567 const digits = e.target.value.replace(/\D/g, ""); setPhone(digits.replace(/(\d{3})(\d{3})(\d{4})/, "$1-$2-$3")); }; return <input value={phone} onChange={handleChange} />; } - Prefer controlled components for forms that need validation.
// Controlled: easy to read and validate before submit const isValid = email.includes("@") && password.length >= 8; <button disabled={!isValid}>Submit</button>
Event Delegation
- Use event delegation for lists of items.
function List({ items }) { const handleItemClick = (e) => { if (e.target.dataset.id) { console.log("Item clicked:", e.target.dataset.id); } }; return ( <ul onClick={handleItemClick}> {items.map(item => ( <li key={item.id} data-id={item.id}> {item.name} </li> ))} </ul> ); } - More efficient than adding listeners to each item.
- Use data-* attributes to identify clicked elements.
- Use e.target.closest() to find the nearest matching ancestor.
function handleClick(e) { const row = e.target.closest("[data-row-id]"); if (row) { console.log("Row clicked:", row.dataset.rowId); } } - Stop bubbling with e.stopPropagation() to prevent parent handlers.
function Card() { return ( <div onClick={handleCardClick}> Card content <button onClick={(e) => { e.stopPropagation(); // don't trigger handleCardClick handleDeleteClick(); }}> Delete </button> </div> ); }
FAQ
You're likely calling the function instead of passing a reference — write onClick={handleClick} not onClick={handleClick()}. If you need to pass arguments, wrap it in an arrow function: onClick={() => handleClick(id)}.
Use e.target.value inside your handler function, e.g. onChange={(e) => setValue(e.target.value)}. For checkboxes, use e.target.checked instead.
preventDefault stops the browser's default behavior (like a form submitting or a link navigating), while stopPropagation stops the event from bubbling up to parent elements. You often need both independently depending on what you're trying to block.
Uncontrolled components (using ref) are useful for file inputs or when integrating with non-React code, since file inputs can't be controlled by React state. For most form fields, controlled components give you real-time access to values for validation and conditional logic.
Add a name attribute to each input and use e.target.name in one handler to update the correct state key: setState(prev => ({ ...prev, [e.target.name]: e.target.value })). This avoids writing a separate handler for every field.