Handle user events in React with camelCase handlers, synthetic events, and controlled inputs.
onClick, not lowercase onclick.function Button() {
function handleClick() {
console.log("Button clicked");
}
return <button onClick={handleClick}>Click me</button>;
}
// Good: pass the function
<button onClick={handleClick}>Click</button>
// Bad: calls function immediately on render
<button onClick={handleClick()}>Click</button>
<button onClick={() => console.log("clicked")}>Click</button>
function List({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>
<button onClick={() => handleDelete(item.id)}>
Delete {item.name}
</button>
</li>
))}
</ul>
);
}
handle prefix so their purpose is obvious at a glance.function handleDelete(id) { /* ... */ }
function handleToggle() { /* ... */ }
onSubmit on the <form> element, not onClick on the button.function LoginForm() {
const handleSubmit = (e) => {
e.preventDefault();
console.log("Form submitted");
};
return <form onSubmit={handleSubmit}></form>;
}
onChange, reading the new value from e.target.value.function NameInput() {
const [name, setName] = useState("");
return <input value={name} onChange={(e) => setName(e.target.value)} />;
}
onMouseEnter, onMouseLeave, and onMouseMove for hover effects.<div onMouseEnter={() => setHovered(true)}>
Hover me
</div>
onKeyDown, checking e.key for the pressed key.function SearchBox() {
const handleKeyDown = (e) => {
if (e.key === "Enter") submitSearch();
if (e.key === "Escape") clearSearch();
};
return <input onKeyDown={handleKeyDown} placeholder="Search..." />;
}
onFocus and onBlur for accessibility cues.function Input() {
const [focused, setFocused] = useState(false);
return (
<input
className={focused ? "focused" : ""}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
/>
);
}
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
}e.nativeEvent.function handleClick(e) {
console.log(e.nativeEvent); // the underlying browser Event
}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);
}const handleChange = (e) => {
setName(e.target.value);
};function Checkbox() {
const [checked, setChecked] = useState(false);
return (
<input
type="checkbox"
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
/>
);
}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
}function handleClick(e) {
console.log(e.target); // element that was clicked
console.log(e.currentTarget); // element with the handler
}function TextInput() {
const [value, setValue] = useState("");
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}function TextInput() {
const inputRef = useRef();
function handleSubmit() {
console.log(inputRef.current.value);
}
return (
<>
<input ref={inputRef} />
<button onClick={handleSubmit}>Submit</button>
</>
);
}// defaultValue sets the initial value but doesn't control updates
<input defaultValue="Alice" ref={inputRef} />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} />;
}// Controlled: easy to read and validate before submit
const isValid = email.includes("@") && password.length >= 8;
<button disabled={!isValid}>Submit</button>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>
);
}function handleClick(e) {
const row = e.target.closest("[data-row-id]");
if (row) {
console.log("Row clicked:", row.dataset.rowId);
}
}function Card() {
return (
<div onClick={handleCardClick}>
Card content
<button onClick={(e) => {
e.stopPropagation(); // don't trigger handleCardClick
handleDeleteClick();
}}>
Delete
</button>
</div>
);
}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.