Learn how React handles user interactions with its synthetic event system, including clicks, form inputs, and keyboard events.
In plain JavaScript, you handle events by calling addEventListener on a DOM element. In React, you handle events directly in JSX using special props that start with on followed by the event name in camelCase.
| Plain JavaScript | React JSX |
|---|---|
element.addEventListener('click', handler) | <button onClick={handler}> |
element.addEventListener('change', handler) | <input onChange={handler}> |
element.addEventListener('submit', handler) | <form onSubmit={handler}> |
element.addEventListener('keydown', handler) | <input onKeyDown={handler}> |
element.addEventListener('mouseover', handler) | <div onMouseOver={handler}> |
Notice the naming convention: all event props use camelCase — onClick not onclick, onChange not onchange, onKeyDown not onkeydown.
Here is a simple click handler:
function App() {
function handleClick() {
alert('Button was clicked!');
}
return <button onClick={handleClick}>Click me</button>;
}Important: You pass the function reference, not a function call:
// CORRECT — passes the function reference
<button onClick={handleClick}>Click</button>
// WRONG — calls the function immediately on render!
<button onClick={handleClick()}>Click</button>With the wrong version, handleClick() runs every time the component renders, not when the button is clicked. This is a very common beginner mistake.
You can also use inline arrow functions for short handlers:
<button onClick={() => console.log('Clicked!')}>Click</button>When an event fires, React passes a SyntheticEvent object to your handler function. This is React's own event object that wraps the browser's native event and provides a consistent API across all browsers.
function App() {
function handleClick(event) {
console.log(event.type); // 'click'
console.log(event.target); // the DOM element clicked
console.log(event.clientX); // mouse X position
}
return <button onClick={handleClick}>Click me</button>;
}The SyntheticEvent has the same interface as the native browser event, so all the properties you know from vanilla JavaScript work: event.target, event.currentTarget, event.type, etc.
event.preventDefault() works the same as in vanilla JavaScript — it stops the browser's default behavior:
function ContactForm() {
function handleSubmit(event) {
event.preventDefault(); // Prevent page reload
console.log('Form submitted!');
}
return (
<form onSubmit={handleSubmit}>
<input type="text" name="email" />
<button type="submit">Submit</button>
</form>
);
}Without preventDefault(), submitting a form causes the browser to reload the page. In a React app, you almost always want to prevent this and handle the submission in JavaScript instead.
event.stopPropagation() prevents the event from bubbling up to parent elements:
function App() {
return (
<div onClick={() => console.log('div clicked')}>
<button onClick={(e) => {
e.stopPropagation();
console.log('button clicked');
}}>
Click me
</button>
</div>
);
}
// Only logs 'button clicked', not 'div clicked'One of the most common event patterns in React is handling text input. You use the onChange event combined with useState to create what React calls a controlled component — an input whose value is driven by React state.
function SearchBox() {
const [query, setQuery] = React.useState('');
function handleChange(event) {
setQuery(event.target.value);
}
return (
<div>
<input
type="text"
value={query}
onChange={handleChange}
/>
<p>Searching for: {query}</p>
</div>
);
}Here is what happens on each keystroke:
onChange event.handleChange calls setQuery(event.target.value) with the new input value.query value.The value={query} attribute makes this a controlled input — React is the single source of truth for the input's value. This gives you full control: you can validate, transform, or reject input before it appears.
// Only allow numbers
function NumberInput() {
const [value, setValue] = React.useState('');
function handleChange(event) {
const newValue = event.target.value;
// Only update if the input contains digits or is empty
if (/^\d*$/.test(newValue)) {
setValue(newValue);
}
}
return <input value={value} onChange={handleChange} />;
}You can also write the handler inline for simpler cases:
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
/>Sometimes you need to pass additional data to an event handler — for example, which item in a list was clicked. The standard pattern is to wrap the handler in an arrow function:
function TodoList() {
const [todos, setTodos] = React.useState([
{ id: 1, text: 'Learn React' },
{ id: 2, text: 'Build a project' },
{ id: 3, text: 'Deploy it' },
]);
function handleDelete(id) {
setTodos(todos.filter(todo => todo.id !== id));
}
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
<button onClick={() => handleDelete(todo.id)}>
Delete
</button>
</li>
))}
</ul>
);
}The arrow function () => handleDelete(todo.id) creates a new function that "closes over" the specific todo.id for each list item. When the button is clicked, it calls handleDelete with the correct ID.
You can still access the event object alongside your custom arguments:
function handleClick(id, event) {
console.log('Item ID:', id);
console.log('Event:', event.type);
}
<button onClick={(event) => handleClick(item.id, event)}>
Click
</button>Common event types you will use often:
onClick — Buttons, clickable elementsonChange — Text inputs, selects, checkboxesonSubmit — FormsonKeyDown / onKeyUp — Keyboard shortcutsonFocus / onBlur — Input focus trackingonMouseEnter / onMouseLeave — Hover effects<div id="root"></div>
<script src="https://unpkg.com/react@19/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@19/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
function App() {
const [query, setQuery] = React.useState("");
const fruits = ["Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape"];
const filtered = fruits.filter(fruit =>
fruit.toLowerCase().includes(query.toLowerCase())
);
return (
<div style={{ padding: "20px" }}>
<h2>Fruit Search</h2>
<input
type="text"
placeholder="Search fruits..."
value={query}
onChange={(e) => setQuery(e.target.value)}
style={{ padding: "8px", fontSize: "16px", width: "200px" }}
/>
<ul>
{filtered.map((fruit, i) => (
<li key={i}>{fruit}</li>
))}
</ul>
<p>{filtered.length} result(s)</p>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>What is the difference between `onClick={handleClick}` and `onClick={handleClick()}`?