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 prop | Fires when… |
|---|---|
onClick | An element is clicked, or a focused button is activated with Enter or Space |
onChange | The value of an input, textarea or select changes – on every keystroke for text |
onSubmit | A form is submitted |
onKeyDown | A key is pressed; read e.key |
onFocus / onBlur | An element gains or loses focus |
onMouseEnter / onMouseLeave | The 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.
