Respond to clicks, typing and form submissions in React, pass arguments to handlers, and understand the synthetic event.

Bishal Biswas
Self Learner
Attaching a handler
function Button() {
function handleClick() {
console.log('Clicked');
}
return <button onClick={handleClick}>Click me</button>;
}
Two differences from HTML:
- The attribute is camelCase, so
onClickrather thanonclick. - The value is a function, not a string of code.
Pass the function, do not call it
<button onClick={handleClick}>Right</button>
<button onClick={handleClick()}>Wrong</button>
The second calls the function during render and passes whatever it returned, usually undefined. If the handler sets state, you get an infinite loop and React throws “Too many re-renders”.
This is one of the most common beginner mistakes.
Passing arguments
You need brackets to pass an argument, so wrap it in an arrow function.
function TodoList({ todos, onDelete }) {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.text}
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
))}
</ul>
);
}
() => onDelete(todo.id) is a new function that, when called, calls onDelete with the id. The arrow is what delays the call.
Inline or named?
<button onClick={() => setCount(count + 1)}>Add</button>
function handleClick() {
setCount(count + 1);
logAnalytics('increment');
showToast('Added');
}
<button onClick={handleClick}>Add</button>
Inline is fine for one short line. Pull it out once the logic grows, because a multi line arrow inside JSX is hard to read.
The event object
Every handler receives an event.
function Form() {
function handleClick(event) {
console.log(event.type);
console.log(event.target);
console.log(event.currentTarget);
}
return <button onClick={handleClick}>Click</button>;
}
event.targetis what was actually clicked, which may be a child element.event.currentTargetis the element the handler is attached to.
With an argument as well, the event comes second:
<button onClick={(event) => handleDelete(event, todo.id)}>Delete</button>
Synthetic events
React wraps the browser’s native event in a SyntheticEvent. It has the same API but behaves identically across browsers.
React also does not attach a listener to every element. It attaches one per event type at the root and works out which component should handle it. You do not need to think about this, but it explains why React apps stay fast with thousands of handlers.
If you need the real browser event:
function handleClick(event) {
console.log(event.nativeEvent);
}
Preventing the default
function LoginForm() {
function handleSubmit(event) {
event.preventDefault();
console.log('Submitting');
}
return (
<form onSubmit={handleSubmit}>
<input name="email" />
<button type="submit">Log in</button>
</form>
);
}
Without preventDefault, the browser reloads the page and your handler’s work is thrown away. This is the number one reason a form handler “does nothing”.
Note the handler goes on the <form> with onSubmit, not on the button with onClick. That way it also fires when the user presses Enter in a field.
Stopping propagation
Events bubble up through parents.
function Modal({ onClose, children }) {
return (
<div className="overlay" onClick={onClose}>
<div className="modal" onClick={(event) => event.stopPropagation()}>
{children}
</div>
</div>
);
}
Clicking the overlay closes the modal. Without stopPropagation, clicking inside the modal would bubble up to the overlay and close it too.
Use it sparingly, because it can silently break other code listening higher up.
The events you will use
<button onClick={handleClick} />
<div onDoubleClick={handleDoubleClick} />
<div onMouseEnter={handleEnter} onMouseLeave={handleLeave} />
<input onChange={handleChange} />
<input onFocus={handleFocus} onBlur={handleBlur} />
<form onSubmit={handleSubmit} />
<input onKeyDown={handleKeyDown} />
onChange is really onInput
In plain HTML, change fires when a field loses focus. In React, onChange fires on every keystroke, which is what you almost always want.
function SearchBox() {
const [query, setQuery] = useState('');
return (
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Search..."
/>
);
}
Keyboard events
function SearchInput({ onSearch }) {
function handleKeyDown(event) {
if (event.key === 'Enter') {
onSearch(event.target.value);
}
if (event.key === 'Escape') {
event.target.blur();
}
}
return <input onKeyDown={handleKeyDown} />;
}
event.key gives you the readable name: 'Enter', 'Escape', 'ArrowDown', 'a'. The old keyCode is deprecated.
Modifier keys are booleans:
if (event.ctrlKey && event.key === 's') {
event.preventDefault();
save();
}
Reading a form without state
function ContactForm() {
function handleSubmit(event) {
event.preventDefault();
const formData = new FormData(event.target);
const data = Object.fromEntries(formData);
console.log(data);
}
return (
<form onSubmit={handleSubmit}>
<input name="email" />
<input name="message" />
<button type="submit">Send</button>
</form>
);
}
Not every input needs state. FormData reads the values on submit, which is simpler when you do not need to react to each keystroke.
Handlers and re-renders
<button onClick={() => setCount(count + 1)}>Add</button>
A new arrow function is created on every render. This is fine. Creating a function is extremely cheap, and it is not something to optimise away by default.
It only matters when the handler is passed to a memoised child, and the React Compiler now handles most of that automatically. There is a whole topic on this later.
Quick recap
- Handlers are camelCase props taking a function.
- Pass
handleClick, neverhandleClick(). - Wrap in an arrow to pass arguments.
event.preventDefault()stops a form reloading the page.- Put
onSubmiton the form, so Enter works too. - React’s
onChangefires on every keystroke. - Use
event.keyfor keyboard checks. - Creating handler functions inline is cheap, so do not optimise prematurely.
import { useState } from 'react';
export default function ContactForm({ onDelete, todos }) {
const [query, setQuery] = useState('');
// Pull a handler out once it does more than one thing
function handleSubmit(event) {
event.preventDefault(); // without this the page reloads and
// your handler's work is thrown away
const data = Object.fromEntries(new FormData(event.target));
console.log('Submitted:', data);
}
function handleKeyDown(event) {
if (event.key === 'Enter') console.log('Searching for', query);
if (event.key === 'Escape') setQuery('');
if (event.ctrlKey && event.key === 's') {
event.preventDefault();
console.log('Save shortcut');
}
}
return (
<div>
{/* onSubmit goes on the form, not the button, so Enter works too */}
<form onSubmit={handleSubmit}>
<input name="email" type="email" />
<input name="message" />
<button type="submit">Send</button>
</form>
{/* React's onChange fires on every keystroke, unlike plain HTML */}
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
onKeyDown={handleKeyDown}
placeholder="Search..."
/>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.text}
{/* An arrow is what delays the call. Writing
onClick={onDelete(todo.id)} would run it during render */}
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
// Stopping propagation: clicking the overlay closes, clicking inside does not
function Modal({ onClose, children }) {
return (
<div className="overlay" onClick={onClose}>
<div className="modal" onClick={(event) => event.stopPropagation()}>
{children}
</div>
</div>
);
}
Was this explanation helpful?
Your feedback helps us improve the content for everyone.
Thank you for your feedback!
We appreciate you helping us improve.
What could we improve? (Optional)
Read aloud
Ready