Every time a user clicks a button, types into a field, or submits a form, the browser fires an event — a signal that something happened. Event handling is how your code listens for those signals and responds to them.
In vanilla JavaScript you'd write something like document.getElementById('btn').addEventListener('click', handleClick). React wraps that system in something cleaner and more consistent, but the underlying idea is the same: you tell React which element to listen to, what kind of event to watch for, and what function to run when it happens.
This is the foundation of interactivity. Without event handling, your UI is a static page. With it, things actually do stuff.
In React, you attach event handlers directly in JSX as props. The prop name is the event type in camelCase, and the value is the function you want to run.
function LikeButton() {
function handleClick() {
console.log('Liked!');
}
return <button onClick={handleClick}>Like this post</button>;
}
A few things worth noting here. The prop is onClick (camelCase), not onclick as you'd see in raw HTML. And the value is {handleClick}: a reference to the function, not a function call. Writing onClick={handleClick()} would call the function immediately when the component renders, which is almost never what you want.
<aside> ⚠️
Warning
onClick={handleClick} ✅ — passes a reference, runs on click
onClick={handleClick()} ❌ — calls the function immediately on render
The same trap exists with inline arrow functions. onClick={() => alert('hi')} ✅ wraps the call in a function. onClick={alert('hi')} ❌ runs alert during render.
</aside>
You can also write the handler inline as an arrow function. This is useful for short, one-liner responses:
<button onClick={() => console.log('Liked!')}>Like this post</button>
For anything more than a single expression, define a named function. It's easier to read, easier to test, and easier to debug.
By convention from the React docs, event handlers are named starting with handle followed by the event: handleClick, handleMouseEnter, handleFormSubmit. You'll see this pattern everywhere in React codebases.
onClick — Responding to ClicksonClick fires when a user clicks any element: buttons, divs, images, links, you name it. In practice, you'll mostly use it on buttons, since those carry the right semantic meaning for users and screen readers.
function ShoppingCart() {
function handleAddToCart() {
console.log('Item added to cart');
// In a real app, this would update cart state
}
return (
<button onClick={handleAddToCart}>
Add to cart
</button>
);
}
<aside> 💡
Info
Buttons are the right element for actions. Avoid putting onClick on a <div> unless you have a specific reason — it breaks keyboard navigation and screen readers. If it does something when clicked, it should be a <button>. You can always style a button to look like anything else with CSS.
</aside>
onChange — Responding to Input ChangesonChange fires every time the value of an input, textarea, or select changes — meaning on every keystroke, not just when the user finishes typing.
function SearchBar() {
function handleSearch(e) {
console.log('Searching for:', e.target.value);
}
return (
<input
type="text"
placeholder="Search products..."
onChange={handleSearch}
/>
);
}
Notice that handleSearch receives e as a parameter. That's the event object (more on that in a moment).
e.target refers to the DOM element that triggered the event (the input in this case), and e.target.value is the current value of that input.
function SubscribeForm() {
function handleEmailChange(e) {
console.log('Email so far:', e.target.value);
}
function handleFrequencyChange(e) {
console.log('Frequency chosen:', e.target.value);
}
return (
<div>
<input
type="email"
placeholder="[email protected]"
onChange={handleEmailChange}
/>
<select onChange={handleFrequencyChange}>
<option value="daily">Daily digest</option>
<option value="weekly">Weekly roundup</option>
<option value="never">Unsubscribe</option>
</select>
</div>
);
}
<aside> ⌨️
Hands On
Create a simple component with a text input and an onChange handler. Log e.target.value to the console. Type something and watch the console update character by character. Now try changing onChange to onBlur — what's different?
</aside>
onSubmit — Responding to Form SubmissiononSubmit fires when a user submits a form, either by clicking a submit button or pressing Enter in a text field.
function ContactForm() {
function handleSubmit(e) {
e.preventDefault(); // Stops the page from reloading — see next section
console.log('Form submitted!');
// In a real app, you'd gather the form data and send it somewhere
}
return (
<form onSubmit={handleSubmit}>
<input type="text" placeholder="Your name" />
<input type="email" placeholder="Your email" />
<button type="submit">Send message</button>
</form>
);
}
Notice that onSubmit goes on the <form> element, not the button. That means the handler fires regardless of how the form was submitted (button click or Enter key).
Every event handler receives an event object as its first argument. React calls these synthetic events — they're React's own version of the browser's native event objects, normalised for consistency across browsers.
By convention, the parameter is typically named e:
function ProfileCard() {
function handleClick(e) {
console.log('Event type:', e.type); // "click"
console.log('Target element:', e.target); // the DOM element clicked
}
return <button onClick={handleClick}>View profile</button>;
}
Here are the properties you'll reach for most often:
| Property | What it gives you |
|---|---|
e.target |
The element that triggered the event |
e.target.value |
The current value of an input element |
e.target.checked |
The checked state of a checkbox |
e.type |
The type of event ("click", "change", etc.) |
e.key |
Which key was pressed (for keyboard events) |
Some HTML elements have built-in browser behaviours that fire alongside your event handler. The two most common ones you'll need to override:
<a> tags) navigate to their hrefe.preventDefault() stops the browser from running its default action.
function LoginForm() {
function handleSubmit(e) {
e.preventDefault(); // Without this, the page would reload
const form = e.target;
const username = form.username.value;
const password = form.password.value;
console.log('Logging in as:', username);
// In a real app: send credentials to your API
}
return (
<form onSubmit={handleSubmit}>
<input name="username" type="text" placeholder="Username" />
<input name="password" type="password" placeholder="Password" />
<button type="submit">Log in</button>
</form>
);
}
<aside> ⚠️
Warning
If you forget e.preventDefault() on a form's onSubmit, your handler will run briefly — but then the page reloads and wipes everything. This is one of the most common React beginner bugs. If your form seems to "do nothing", check for this first.
</aside>
<aside> ⌨️
Hands On
Build a form with a single email input and a submit button. In the onSubmit handler:
e.preventDefault()e.target.email.value (this works when the input has name="email")Then try removing e.preventDefault() and observe what happens.
</aside>
Here's something that surprises a lot of developers when they first encounter it. Event handlers don't just catch events on their own element — they also catch events from any child elements nested inside them. The event "bubbles" or "propagates" upward through the tree, from where it started all the way to the top.
Consider this toolbar:
function Toolbar() {
return (
<div onClick={() => console.log('Toolbar was clicked')}>
<button onClick={() => console.log('Play clicked')}>
Play Movie
</button>
<button onClick={() => console.log('Upload clicked')}>
Upload Image
</button>
</div>
);
}
If you click the "Play Movie" button, you'll see two messages in the console — first 'Play clicked' from the button's own handler, then 'Toolbar was clicked' from the parent <div>. The click event bubbles up from the button to the div.
If you click the toolbar area itself (not a button), only 'Toolbar was clicked' appears.
<aside> 💡
Info
All events propagate in React except onScroll, which only fires on the element it's directly attached to.
</aside>
If you want an event to stay on the element that triggered it and not travel to parent handlers, call e.stopPropagation():
function Button({ onClick, children }) {
return (
<button
onClick={(e) => {
e.stopPropagation(); // Stop the event from reaching parent elements
onClick(); // Still call the handler passed in from the parent
}}
>
{children}
</button>
);
}
function Toolbar() {
return (
<div onClick={() => console.log('Toolbar clicked')}>
<Button onClick={() => console.log('Playing!')}>
Play Movie
</Button>
<Button onClick={() => console.log('Uploading!')}>
Upload Image
</Button>
</div>
);
}
Now clicking a button logs only that button's message. The toolbar handler never fires.
<aside> ⚠️
</aside>
The React docs suggest a pattern worth knowing: rather than relying on events bubbling up silently, pass explicit handler props and call them manually. This gives you complete control and makes the chain of events easy to trace.
function Button({ onClick, children }) {
return (
<button
onClick={(e) => {
e.stopPropagation();
onClick(); // Explicitly call the parent's handler
}}
>
{children}
</button>
);
}
You can add logic before or after calling onClick(), and you always know exactly what runs and in what order (which makes debugging much easier than following implicit bubbling behaviour).
<aside> ⌨️
</aside>
As your component tree grows, you'll often want a parent component to decide what happens when something in a child component is interacted with. React handles this by passing event handlers as props.
// Button doesn't know what happens on click — it just passes it through
function Button({ onClick, children }) {
return (
<button onClick={onClick}>
{children}
</button>
);
}
function MediaToolbar() {
function handlePlayClick() {
console.log('Starting playback...');
}
return (
<div>
<Button onClick={handlePlayClick}>Play Movie</Button>
<Button onClick={() => console.log('Uploading...')}>Upload Image</Button>
</div>
);
}
The Button component is reusable because it owns the look, not the behaviour. The parent decides what clicking it does. This is a pattern you'll use constantly when building component libraries or design systems.
For built-in elements like <button> and <input>, React uses the standard browser event names: onClick, onChange, onSubmit. When you're building your own components and exposing event-like props, follow the same convention: start with on, followed by a capital letter.
// ✅ Good — consistent with React's naming convention
function VideoPlayer({ onPlay, onPause }) {
return (
<div>
<button onClick={onPlay}>Play</button>
<button onClick={onPause}>Pause</button>
</div>
);
}
// Usage
<VideoPlayer
onPlay={() => console.log('Playing')}
onPause={() => console.log('Paused')}
/>
Naming props after what they represent in your app (onPlay, onDeleteItem, onCheckout) rather than generic actions (onClick1, onClick2) makes your components more readable and easier to compose.
Sometimes you need to pass extra data to a handler: for example, which item in a list was clicked. You can do this by wrapping the call in an arrow function.
function ProductList() {
const products = [
{ id: 1, name: 'Wireless headphones' },
{ id: 2, name: 'Mechanical keyboard' },
{ id: 3, name: 'USB-C hub' },
];
function handleAddToCart(productId) {
console.log('Added product ID:', productId);
}
return (
<ul>
{products.map((product) => (
<li key={product.id}>
{product.name}
<button onClick={() => handleAddToCart(product.id)}>
Add to cart
</button>
</li>
))}
</ul>
);
}
The arrow function () => handleAddToCart(product.id) creates a new function that closes over the current product.id — a concept called closure, which you covered in the core course. React calls that new function when the button is clicked, which in turn calls handleAddToCart with the right ID.
<aside> ⌨️
</aside>
One thing worth calling out explicitly: event handlers are the right place for side effects in React. A side effect is anything that reaches outside the current function: logging, network requests, updating the DOM, changing data.