- Home
- /
- Tutorials
- /
- React Tutorial
- /
- Handling Events
Handling Events
Handling Events
You pass a function to an event prop. Calling it yourself - the missing arrow - is the most common React mistake there is.
Pass the function, do not call it
onClick={handleClick} hands React the function to run later. onClick={handleClick()} runs it immediately during render and gives React whatever it returned.
The missing arrow, shown
jsx
// Counts how many times the function has actually run.
let calls = 0
function greet() {
calls = calls + 1
return "a string, not a handler"
}
function Buttons() {
const [clicks, setClicks] = React.useState(0)
return (
<div>
{/* WRONG: greet() runs right now, during render. React receives
its return value — a string — which is not a handler at all. */}
<button onClick={greet()}>Wrong — already ran</button>{" "}
{/* RIGHT: React receives the function and calls it on click. */}
<button onClick={() => { greet(); setClicks((c) => c + 1) }}>
Right — runs on click
</button>
<p>greet() has run {calls} time(s). Clicks handled: {clicks}.</p>
<p style={{ color: "#64748b" }}>
Press the left button: nothing happens. Press the right one: the count moves.
</p>
</div>
)
}
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<Buttons />)Run it and the counter already reads 1 before you touch anything - greet() ran during render. The left button is now wired to a string, so clicking it does nothing at all.
When the handler needs no arguments you can pass it directly: onClick={handleClick}. The arrow wrapper is only needed when you have to pass something.
Passing arguments
An arrow function is how you supply arguments without calling the handler early. The arrow is created on each render and called on each click.
Arguments through an arrow
jsx
function Picker() {
const [chosen, setChosen] = React.useState("none")
const colours = ["red", "green", "blue"]
return (
<div>
<p>Chosen: {chosen}</p>
{colours.map((colour) => (
<button key={colour} onClick={() => setChosen(colour)} style={{ marginRight: 6 }}>
{colour}
</button>
))}
</div>
)
}
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<Picker />)The event object
React passes a synthetic event - a wrapper with the same API as the browser's event, normalised across browsers. preventDefault and stopPropagation work exactly as you expect.
Reading the event
jsx
function Form() {
const [text, setText] = React.useState("")
const [submitted, setSubmitted] = React.useState("")
function handleSubmit(event) {
event.preventDefault() // without this the page reloads
setSubmitted(text)
}
return (
<form onSubmit={handleSubmit}>
<input value={text} onChange={(event) => setText(event.target.value)} />{" "}
<button type="submit">Send</button>
<p>Submitted: {submitted || "nothing yet"}</p>
</form>
)
}
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<Form />)Naming and the small print
- Event props are camelCase:
onClick,onChange,onSubmit,onMouseEnter. - Handler functions are conventionally named
handleX; the prop that receives one is namedonX. - Returning
falsedoes not cancel anything - callpreventDefault. - React attaches one listener at the root rather than one per element, which is why adding thousands of handlers is cheap.
Handlers are recreated every render
The arrow you write inline is a brand new function on each render. That is normal and almost never a problem - React is not comparing handlers, and creating a function is cheap.
It only starts to matter when a child is memoised and a fresh function defeats that memoisation. That is a performance concern with a specific fix, covered later; reaching for useCallback before you have measured a problem adds noise for nothing.
