Attaching Handlers in JSX
In plain JavaScript you find an element and call addEventListener on it. In React you never do that. You write the handler as a prop directly on the element, and React manages the wiring — including removing the listener when the element leaves the screen, which is a leak you had to remember to fix by hand before.
Two rules cover the syntax. Event names are camelCase: onClick, onChange, onSubmit, onMouseEnter, onKeyDown. And the value is a real function, not a string of code — onClick={handleClick}, never onclick="handleClick()". Get the capitalisation wrong and React will warn you in the console that onclick is not a valid DOM property, which is a friendlier failure than most.
Handlers are normally declared inside the component, above the return. That placement is not arbitrary: it means the handler can read the component's props and state directly, with no wiring, because it is simply a function closing over them.
function ContactForm() {
const [email, setEmail] = useState('');
// Handlers are ordinary functions declared inside the component.
function handleChange(e) {
setEmail(e.target.value);
}
function handleSubmit(e) {
e.preventDefault(); // stop the browser reloading the page
console.log('Sending', email);
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={handleChange}
onFocus={() => console.log('focused')}
placeholder="you@example.com"
/>
<button type="submit">Send</button>
</form>
);
} onClick— a click or taponChange— an input's value changed (in React this fires on every keystroke)onSubmit— a form was submitted, by button or by EnteronKeyDown/onKeyUp— keyboard input; checke.keyonMouseEnter/onMouseLeave— hover, without CSSonFocus/onBlur— an element gained or lost focus
- React's
onChangebehaves like the DOM'sinputevent, firing on every keystroke rather than only when the field loses focus. If you want the value once the user has finished, useonBlur.
Pass the Function, Do Not Call It
This is the mistake every React beginner makes, usually within their first hour. onClick={handleDelete} hands React the function so it can call it when a click happens. onClick={handleDelete()} calls it right now, while the component is rendering, and gives React whatever it returned — usually undefined.
The symptoms vary and none of them mentions the real cause. Sometimes the action fires on page load: every row in a list deletes itself the moment it appears. Sometimes clicking does nothing at all, because React was handed undefined. And sometimes the app freezes in an endless loop, because the handler set some state, which caused a render, which called the handler again.
The confusion arises because you often do need to pass an argument, and handleDelete(id) looks like the only way. The answer is to wrap it in an arrow function: onClick={() => handleDelete(id)}. That creates a new function which, when React eventually calls it, calls yours with the argument. You are still passing a function; you are just passing a different one.
function OrderRow({ order, onCancel }) {
return (
<tr>
<td>{order.id}</td>
{/* Correct: pass the reference */}
<td><button onClick={onCancel}>Cancel</button></td>
{/* Correct: wrap in an arrow when you need an argument */}
<td><button onClick={() => onCancel(order.id)}>Cancel this one</button></td>
{/* WRONG: runs during render, for every row, immediately */}
<td><button onClick={onCancel(order.id)}>Broken</button></td>
</tr>
);
}
// Watch the difference in a list of 20 orders:
// correct -> nothing happens until a click
// wrong -> all 20 cancel as soon as the table renders - You may read that inline arrow functions hurt performance because a new function is created on every render. For ordinary UI this is not measurable and the readability is worth far more. It becomes relevant only in specific, measured cases, which the performance lesson covers with
useCallback.
The Event Object
Every handler receives an event object as its first argument, conventionally named e. React passes a wrapper around the browser's native event, called a SyntheticEvent, which behaves the same way across browsers. Everything you already know about DOM events applies: e.target, e.preventDefault(), e.stopPropagation(), e.key.
preventDefault is the one you cannot skip. A <form> submitting reloads the page by default, which in a React app means your entire application restarts and all state is lost. A form that seems to wipe itself clean and flash on submit is always a missing e.preventDefault().
e.target and e.currentTarget are easy to mix up and the difference matters. target is the element the event actually started on, which might be an icon inside your button. currentTarget is the element whose handler is running. If you attach a click handler to a card containing an image and read e.target, you may get the image; e.currentTarget reliably gives you the card.
One practical caution: if your handler is asynchronous — it awaits something, or schedules a setTimeout — read the values you need out of the event first and store them in variables. Depending on the event object after the handler has finished is fragile, and const value = e.target.value on the first line costs nothing.
function SearchBar({ onSearch }) {
function handleKeyDown(e) {
if (e.key === 'Enter') {
onSearch(e.currentTarget.value);
}
if (e.key === 'Escape') {
e.currentTarget.blur();
}
}
async function handleSubmit(e) {
e.preventDefault();
const value = e.target.query.value; // read it out FIRST
await sendToServer(value); // then await
}
return (
<form onSubmit={handleSubmit}>
<input name="query" onKeyDown={handleKeyDown} />
</form>
);
}
// Stopping a click from reaching the parent
function ProductCard({ product, onOpen, onFavourite }) {
return (
<div className="card" onClick={() => onOpen(product.id)}>
<h3>{product.name}</h3>
<button
onClick={(e) => {
e.stopPropagation(); // without this, the card also opens
onFavourite(product.id);
}}
>
♥
</button>
</div>
);
} e.target— where the event started (may be a child element)e.currentTarget— the element whose handler is runninge.target.value— an input's current texte.preventDefault()— stop form submission or link navigatione.stopPropagation()— stop the event bubbling to parent handlerse.key— the key pressed, as a string such as'Enter'or'Escape'
- React does not attach a listener to every element. It listens near the root of your app and routes events to the right component. You rarely need to know this, but it explains why mixing a manual
addEventListenerwith React handlers on the same element can produce surprising ordering.
The Loop: Event, State, Re-render
Almost every interactive feature you build follows the same three-step cycle. The user does something, the handler updates state, React re-renders with the new state. Once you see that loop, most React code stops looking mysterious — it is that shape repeated with different details.
The quantity stepper below is a small, complete example. Notice that nothing in the handlers touches the DOM. They only change numbers. The buttons being disabled at the limits, the total updating, the text changing — all of it falls out of the render, calculated from qty.
Notice too where the different kinds of logic live. Validation and clamping happen in the handler, because they are responses to a user action. The subtotal is calculated during render, because it is derived from state. Nothing is stored twice. That division — handlers change state, render calculates everything else — is the habit worth building.
function QuantityStepper({ product, max = 10 }) {
const [qty, setQty] = useState(1);
function increase() {
setQty(q => Math.min(q + 1, max));
}
function decrease() {
setQty(q => Math.max(q - 1, 1));
}
function handleTyped(e) {
const n = Number(e.target.value);
if (Number.isNaN(n)) return;
setQty(Math.min(Math.max(n, 1), max));
}
const subtotal = qty * product.price; // derived, not stored
return (
<div>
<button onClick={decrease} disabled={qty === 1}>−</button>
<input value={qty} onChange={handleTyped} size={3} />
<button onClick={increase} disabled={qty === max}>+</button>
<p>Subtotal: ₹{subtotal}</p>
{qty === max && <p>Maximum {max} per order.</p>}
</div>
);
} - Handlers are the correct place for things that should happen because the user did something: sending a form, opening a modal, saving to
localStorage. Do not move that logic into an effect just because it involves the outside world — the effects lesson explains why that usually makes things worse.
Mistakes Worth Recognising Quickly
Event bugs in React are a small, repetitive set. Learning to recognise the symptom saves far more time than learning the theory, so here is the list in the form you will actually meet it — as a symptom first.
One entry deserves expanding. If a button inside a form does something unexpected, check its type. A <button> with no type attribute defaults to type="submit", so a Cancel button or a Show Password toggle placed inside a form will submit the form when clicked. Set type="button" on every button in a form that is not meant to submit it.
- The page flashes and state resets on submit — missing
e.preventDefault() - The action fires as soon as the page loads — you wrote
onClick={fn()}instead ofonClick={fn} - Clicking does nothing — a lowercase
onclick, or a handler that returns instead of acting - A button in a form behaves like submit — add
type="button" - Clicking a child also triggers the parent — add
e.stopPropagation()in the child's handler - The handler sees an old value — a stale closure; use the updater form of the setter
- Typing feels wrong or the cursor jumps — usually a controlled-input problem, covered in the forms lesson
- Interactive elements should be real interactive elements. A
<div onClick>cannot be reached by keyboard and is invisible to screen readers. Use<button>for actions and<a>for navigation, and style them — it costs you nothing and makes the app usable for everyone.
