Handling Events

Event handlers, passing arguments, and how React's events differ from the DOM.

What you'll learn

  • Attaching a handler, and the difference between passing and calling
  • Passing arguments to a handler
  • The event object, preventDefault, and bubbling
  • Where handlers belong as components grow

Attaching a handler

Event props are camelCased — onClick, onChange, onSubmit — and they take a function, not a string:

export default function App() {
  function handleClick() {
    console.log("clicked");
  }

  return (
    <div style={{ fontFamily: "sans-serif", padding: 16 }}>
      <button onClick={handleClick}>Named handler</button>
      <button onClick={() => console.log("inline")} style={{ marginLeft: 8 }}>
        Inline handler
      </button>
    </div>
  );
}

Both forms are ordinary. Use a named function when the logic is more than a line; use an inline arrow when it is short or needs an argument.

Pass the function, do not call it

This is the most common beginner bug in React:

<button onClick={handleClick}>   // right: React calls it on click
<button onClick={handleClick()}> // wrong: called during render

The second one runs handleClick while rendering and passes its return value — usually undefined — as the handler. If that function sets state, you get an infinite loop of renders.

export default function App() {
  function sayHello() {
    console.log("sayHello ran");
    return "not a handler";
  }

  return (
    <div style={{ fontFamily: "sans-serif", padding: 16 }}>
      {/* Runs immediately on render — watch the console before clicking. */}
      <button onClick={sayHello()}>Broken</button>

      <button onClick={sayHello} style={{ marginLeft: 8 }}>Works</button>
    </div>
  );
}

The console logs before you touch anything — that is the broken button firing during render. Clicking it does nothing, and React warns that it received a string where it expected a function.

It logs twice, which is worth knowing now: in development React renders each component twice on purpose, so that side effects like this one are obvious rather than silent. In production it renders once.

Passing arguments

Because the handler must be a function, wrap the call in an arrow when you need to pass something:

const products = ["Book", "Pen", "Desk"];

export default function App() {
  function addToCart(name) {
    console.log("added:", name);
  }

  return (
    <div style={{ fontFamily: "sans-serif", padding: 16, display: "flex", gap: 8 }}>
      {products.map((name) => (
        <button key={name} onClick={() => addToCart(name)}>
          {name}
        </button>
      ))}
    </div>
  );
}

() => addToCart(name) creates a function that, when called, calls addToCart(name). That is the whole pattern.

The event object

React passes an event object to your handler. It is a synthetic event — a wrapper with the same interface as the DOM event, normalised across browsers:

import { useState } from "react";

export default function App() {
  const [text, setText] = useState("");

  return (
    <div style={{ fontFamily: "sans-serif", padding: 16 }}>
      <input
        value={text}
        placeholder="Type here"
        onChange={(event) => setText(event.target.value)}
      />
      <p>Value: {text}</p>

      <button onClick={(event) => console.log("button type:", event.type)}>
        Log the event type
      </button>
    </div>
  );
}

event.target.value is how you read what the user typed. That input is a controlled component — its value comes from state — which is the subject of Forms and Controlled Inputs.

preventDefault

Returning false does nothing in React. To stop the browser's default behaviour, call preventDefault — most often to stop a form reloading the page:

import { useState } from "react";

export default function App() {
  const [name, setName] = useState("");

  function handleSubmit(event) {
    event.preventDefault(); // without this, the page reloads
    console.log("submitted:", name);
  }

  return (
    <form onSubmit={handleSubmit} style={{ fontFamily: "sans-serif", padding: 16 }}>
      <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" />
      <button type="submit" style={{ marginLeft: 8 }}>Submit</button>
    </form>
  );
}

Note the handler is on the <form>, not the button. That way it also fires when the user presses Enter in the field.

Events bubble

An event travels up through ancestors, so a click on the button below also reaches the surrounding div. stopPropagation ends that journey:

export default function App() {
  return (
    <div
      onClick={() => console.log("outer div was clicked")}
      style={{ fontFamily: "sans-serif", padding: 16, border: "1px solid #ddd" }}
    >
      <p style={{ marginTop: 0 }}>The box below is inside a clickable div.</p>

      <button onClick={() => console.log("bubbles up")}>Bubbles</button>

      <button
        onClick={(event) => {
          event.stopPropagation();
          console.log("stops here");
        }}
        style={{ marginLeft: 8 }}
      >
        Stops
      </button>
    </div>
  );
}

Bubbling is why you can put one handler on a list instead of one per row.

Common mistakes

  • onClick={handler()}. Calls it during render. Drop the parentheses.
  • onclick. React uses camelCase: onClick.
  • return false. Use event.preventDefault().
  • A handler on the submit button. Put onSubmit on the form so Enter works.

Exercise

Three bugs: the handler is called during render, the argument is passed wrongly, and the form reloads the page on submit. Fix all three — the console should log only when you click.

export default function App() {
  function log(message) {
    console.log("logged:", message);
  }

  function handleSubmit() {
    console.log("submitted");
  }

  return (
    <form onSubmit={handleSubmit} style={{ fontFamily: "sans-serif", padding: 16 }}>
      <button type="button" onClick={log("clicked")}>Log</button>
      <button type="submit" style={{ marginLeft: 8 }}>Submit</button>
    </form>
  );
}