Lesson 7 of 20

Handling Events

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.

Example
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 tap
  • onChange — an input's value changed (in React this fires on every keystroke)
  • onSubmit — a form was submitted, by button or by Enter
  • onKeyDown / onKeyUp — keyboard input; check e.key
  • onMouseEnter / onMouseLeave — hover, without CSS
  • onFocus / onBlur — an element gained or lost focus
Notes
  • React's onChange behaves like the DOM's input event, firing on every keystroke rather than only when the field loses focus. If you want the value once the user has finished, use onBlur.

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.

Example
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
Notes
  • 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.

Example
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 running
  • e.target.value — an input's current text
  • e.preventDefault() — stop form submission or link navigation
  • e.stopPropagation() — stop the event bubbling to parent handlers
  • e.key — the key pressed, as a string such as 'Enter' or 'Escape'
Notes
  • 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 addEventListener with 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.

Example
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>
  );
}
Notes
  • 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 of onClick={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
Notes
  • 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.
Ask AI