Lesson 6 of 12

React Events

You have already used onClick a few times to change state. This lesson looks at React events properly: how to attach handlers, how to pass extra information, what the event object gives you, and how a child component can tell its parent that something happened. If you know JavaScript Events, most of the ideas will feel familiar – React simply puts them in your JSX.

Adding a handler for React events

In React you add a handler as a prop on the element, in camelCase: onClick, onChange, onSubmit. The value is a function in curly braces. The usual pattern is to define a function inside your component, named handle plus the event, and pass it by name.

function OrderButton() {
  function handleClick() {
    console.log("Order placed");
  }

  return (
    <>
      {/* Right: React calls handleClick when the button is clicked */}
      <button onClick={handleClick}>Order</button>

      {/* Wrong: handleClick() runs immediately, during rendering */}
      <button onClick={handleClick()}>Order</button>

      {/* Right: an inline arrow function, handy for short code */}
      <button onClick={() => console.log("Order placed")}>Order</button>
    </>
  );
}

The second button is a classic beginner mistake. The brackets call the function while React is rendering, not when the user clicks. If that function sets state, React renders again, calls it again, and you get an endless loop with the error “Too many re-renders”.

Passing arguments to a handler

Often one handler serves several buttons and needs to know which one was pressed. Wrap the call in an arrow function: onClick={() => chooseCity("Kochi")}. The arrow function is created during rendering but only runs on click, and then it calls your handler with the argument. The example also uses map() to create one button per city; the key attribute is explained in Lists and Keys.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>One handler, several buttons</title>
  <script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
  <style>
    body { font-family: system-ui, sans-serif; padding: 16px; line-height: 1.5; }
    button { margin: 4px 6px 4px 0; padding: 6px 12px; border: 1px solid #999; border-radius: 6px; background: #fff; }
    button.active { background: #0b6e99; color: #fff; border-color: #0b6e99; }
  </style>
</head>
<body>
  <div id="root"></div>

  <script type="text/babel" data-type="module">
import React, { useState } from "https://esm.sh/[email protected]";
import { createRoot } from "https://esm.sh/[email protected]/client";

const cities = ["Chennai", "Kochi", "Hyderabad", "Mysuru"];

function App() {
  const [city, setCity] = useState("Chennai");

  function chooseCity(name) {
    setCity(name);
  }

  return (
    <div>
      <p>Show workshops in:</p>
      {cities.map(name => (
        <button
          key={name}
          type="button"
          className={name === city ? "active" : ""}
          aria-pressed={name === city}
          onClick={() => chooseCity(name)}
        >
          {name}
        </button>
      ))}
      <p>Showing workshops in <strong>{city}</strong>.</p>
    </div>
  );
}

createRoot(document.getElementById("root")).render(<App />);
  </script>
</body>
</html>

The event object

React passes an event object to your handler as its first argument, usually called e. It is a wrapper around the browser’s native event that works the same way in every browser, and it has the familiar properties: e.target, e.key, e.preventDefault() and e.stopPropagation().

One difference between React events and plain JavaScript surprises people: React’s onChange on a text field fires on every keystroke, just like the native input event. In plain JavaScript, change waits until the field loses focus.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Typing, keys and the event object</title>
  <script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
  <style>
    body { font-family: system-ui, sans-serif; padding: 16px; line-height: 1.5; }
    input { padding: 6px; width: 240px; }
    .log { color: #555; font-size: 14px; }
  </style>
</head>
<body>
  <div id="root"></div>

  <script type="text/babel" data-type="module">
import React, { useState } from "https://esm.sh/[email protected]";
import { createRoot } from "https://esm.sh/[email protected]/client";

function App() {
  const [text, setText] = useState("");
  const [lastKey, setLastKey] = useState("none yet");
  const [sent, setSent] = useState([]);

  function handleChange(e) {
    setText(e.target.value);
  }

  function handleKeyDown(e) {
    setLastKey(e.key);
    if (e.key === "Enter" && text.trim() !== "") {
      setSent([...sent, { id: sent.length + 1, text: text.trim() }]);
      setText("");
    }
  }

  return (
    <div>
      <label htmlFor="msg">Message (press Enter to send)</label><br />
      <input id="msg" value={text} onChange={handleChange} onKeyDown={handleKeyDown} />
      <p className="log">Characters: {text.length} · Last key: {lastKey}</p>
      <ol>
        {sent.map(m => <li key={m.id}>{m.text}</li>)}
      </ol>
    </div>
  );
}

createRoot(document.getElementById("root")).render(<App />);
  </script>
</body>
</html>
React propFires when…
onClickAn element is clicked, or a focused button is activated with Enter or Space
onChangeThe value of an input, textarea or select changes – on every keystroke for text
onSubmitA form is submitted
onKeyDownA key is pressed; read e.key
onFocus / onBlurAn element gains or loses focus
onMouseEnter / onMouseLeaveThe pointer moves onto or off an element

Propagation and default actions

React events bubble up the tree just like DOM events: a click on a button inside a card also reaches the card’s onClick. Call e.stopPropagation() in the inner handler if the outer one should not run. Separately, e.preventDefault() stops the browser’s built-in action, such as a form reloading the page or a link navigating away. You will use it constantly in React Forms.

Passing handlers as props

A reusable component should not decide what happens when it is clicked – its parent should. So the parent passes a function as a prop, and the child attaches it to the real element. By convention, such props are named on plus a description, such as onAdd or onDelete, while the functions that implement them are named handleAdd and so on.

function AddButton({ item, onAdd }) {
  return <button onClick={() => onAdd(item)}>Add {item}</button>;
}

function Cart() {
  const [items, setItems] = useState([]);

  function handleAdd(item) {
    setItems([...items, item]);
  }

  return (
    <>
      <AddButton item="Jasmine tea" onAdd={handleAdd} />
      <AddButton item="Banana chips" onAdd={handleAdd} />
      <p>In cart: {items.join(", ") || "nothing yet"}</p>
    </>
  );
}

Event handlers are also the right place for side effects – work that changes something outside the component, such as saving to a server, playing a sound or updating state. Unlike the component body, handlers do not run during rendering, so they do not need to be pure. When something should happen because the user did something, it belongs in a handler.

Tip: Attach click handlers to real <button> elements rather than to a <div>. Buttons can be reached with Tab, activated with the keyboard and are announced properly by screen readers, with no extra work from you.

Further reading

Key takeaways

  • Add handlers as camelCase props: onClick={handleClick}.
  • Pass the function itself; onClick={handleClick()} calls it during rendering.
  • Use an arrow function to pass arguments: onClick={() => choose(id)}.
  • The handler receives an event object with target, key, preventDefault() and stopPropagation().
  • React's onChange fires on every keystroke in a text field.
  • Pass functions as props (onAdd, onDelete) so children can tell parents what happened.

Quick quiz

Check what you learned – pick an answer.

1. Which line runs removeItem(5) only when the button is clicked?

2. When does React's onChange fire on a text input?

3. What is the conventional name for a prop that passes a "delete" handler to a child?

Practice exercise

Make a colour picker with three buttons – Teal, Saffron and Indigo. Clicking a button changes the background colour of a box below and shows the chosen colour name in the box. Use one handler that receives the colour as an argument.

Show a hint

Store the chosen colour in state, then use style={{ background: colour }} on the box.

Show the solution
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Colour picker</title>
  <script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
  <style>
    body { font-family: system-ui, sans-serif; padding: 16px; line-height: 1.5; }
    .box { margin-top: 12px; padding: 24px; border-radius: 8px; color: #fff; font-weight: bold; }
    button { margin-right: 6px; }
  </style>
</head>
<body>
  <div id="root"></div>

  <script type="text/babel" data-type="module">
import React, { useState } from "https://esm.sh/[email protected]";
import { createRoot } from "https://esm.sh/[email protected]/client";

const colours = { Teal: "#0f766e", Saffron: "#d97706", Indigo: "#4338ca" };

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

  function handlePick(colourName) {
    setName(colourName);
  }

  return (
    <div>
      {Object.keys(colours).map(c => (
        <button key={c} type="button" onClick={() => handlePick(c)}>{c}</button>
      ))}
      <div className="box" style={{ background: colours[name] }}>{name}</div>
    </div>
  );
}

createRoot(document.getElementById("root")).render(<App />);
  </script>
</body>
</html>

Progress is saved in this browser only.