🎓 What You Will Learn
- How React events differ from HTML: camelCase names, functions instead of strings, and the SyntheticEvent wrapper
- Wiring your first onClick: pass the function, never call it
- Passing arguments to handlers: the arrow-wrapper pattern (and the classic bug it fixes)
- The event object:
e.targetvse.currentTarget, plus TypeScript event types - Form, keyboard, mouse, and focus events:
onChange,onSubmit,onKeyDown,onMouseEnter,onFocus,onBlur - Event propagation: bubbling,
stopPropagation(), and the delegation mental model - Events + state together: building a real star-rating widget
Prerequisite: you should be comfortable with useState — every event handler here updates state.
Why Events in React Are Different
If you learned HTML first, you probably wrote onclick="doSomething()" — a lowercase attribute holding a string of JavaScript. React looks similar but works differently in three important ways, and mixing them up is the #1 source of beginner event bugs.
| Plain HTML | React JSX | |
|---|---|---|
| Attribute name | onclick | onClick |
| What you pass | A string of code | A function reference |
| Event object | Native browser Event | SyntheticEvent (a consistent wrapper) |
| Where listeners attach | On each element | One delegated listener at the React root |
<!-- Plain HTML: lowercase attribute, string of code -->
<button onclick="handleClick()">Click me</button>
// React JSX: camelCase prop, function reference
<button onClick={handleClick}>Click me</button>Ang importante dito: in React you hand over the function itself (handleClick, walang parentheses) and React calls it for you when the event fires. If you write handleClick(), you are calling it right now, during render — we will see that bug live in Section 3.
SyntheticEvent object so it behaves identically in every browser. It has the same API you already know — e.target, e.preventDefault(), e.stopPropagation() — and if you ever need the raw browser event, it's available at e.nativeEvent.Your First onClick
The smallest useful event: a button that counts its own clicks. Three steps, and this exact shape covers 80% of the event handling you will ever write.
You clicked 0 times
Click the button in the Demo tab, then open the Code tab. The flow is always the same: an event fires → your handler runs → the handler calls a state setter → React re-renders with the new value. That loop — event → state → render — is the heartbeat of every React app.
handleClick, handleChange, handleSubmit — the handle prefix instantly tells anyone reading your code "this function responds to an event." The matching prop is always on + the event name.Passing Arguments to Handlers
Sooner or later you need to tell the handler which item was clicked — which fruit, which product, which row. The instinct is to write onClick={handlePick(fruit)}. Here is what that actually does:
// ❌ BROKEN: handlePick runs immediately, during render — not on click!
export function FruitPicker() {
const [picked, setPicked] = useState('');
const handlePick = (fruit: string) => {
setPicked(fruit); // setting state during render...
};
return (
<div>
{/* This CALLS handlePick('Mango') while rendering. */}
{/* Setting state during render triggers another render, */}
{/* which calls it again... hello, infinite loop: */}
{/* "Too many re-renders. React limits the number of renders */}
{/* to prevent an infinite loop." */}
<button onClick={handlePick('Mango')}>Mango</button>
</div>
);
}onClick={handlePick('Mango')} calls the function during render and passes its return value (here, undefined) to onClick. If the handler sets state, you get React's famous crash: "Too many re-renders. React limits the number of renders to prevent an infinite loop." Kapag nakita mo ang error na 'yan, check your onClick props first.The fix: wrap the call in an arrow function. Now you are passing a new small function whose body only runs when the click actually happens.
// ✅ FIXED: pass a function that, WHEN CALLED, runs handlePick('Mango')
<button onClick={() => handlePick('Mango')}>Mango</button>Pick a fruit
onClick wants a recipe, not a cooked meal. handlePick(fruit) cooks the meal immediately; () => handlePick(fruit) hands React the recipe card so it can cook later, on click. The arrow wrapper is re-created on each render — totally fine for most apps; measuring and fixing that cost is a useCallback topic for later.The Event Object: e.target, e.currentTarget, and Types
Every handler receives an event object (by convention named e) as its first argument. React passes it automatically — you don't do anything special to get it.
export function EventInspector() {
// React passes the event object automatically
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.type); // "click"
console.log(e.currentTarget); // the <button> the handler is attached to
console.log(e.target); // the exact element that was clicked
console.log(e.clientX, e.clientY); // mouse position in the viewport
};
return (
<button onClick={handleClick}>
<span>Icon</span> Inspect me
</button>
);
}The pair that confuses everyone: e.target vs e.currentTarget. In the button above, clicking the <span> inside it makes e.target the span (what you actually hit) while e.currentTarget is always the button (what the handler is attached to). Rule of thumb: reaching for the element you attached the handler to? Use currentTarget. Reading an input's value in onChange? e.target.value is the idiom.
If you write TypeScript, React ships precise types for each event family — your editor then autocompletes exactly the right properties:
// Mouse events (click, double-click, mouse enter/leave)
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.clientX, e.clientY);
};
// Change events (inputs, selects, textareas)
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value); // typed as string ✨
};
// Form submission
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
};
// Keyboard events
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
console.log(e.key); // "Enter", "Escape", "a", "ArrowDown"...
};
// Focus events
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
console.log('left the field with value:', e.target.value);
};Form & Input Events: onChange, onSubmit, preventDefault
Forms are where events earn their keep. Two events do almost all the work: onChange fires on every keystroke (unlike plain HTML's change event, which waits for blur), and onSubmit fires when the form is submitted — by the button or by pressing Enter inside a field.
Type your name, then press Sign up (or Enter)
Try it: type your name and press Enter — the form submits without touching the button, dahil onSubmit lives on the <form>, hindi sa button. That's also why you put the handler on the form, not onClick on the submit button.
e.preventDefault(): a browser's default behavior for form submission is to reload the whole page — which wipes out all your React state. If your form "flashes and resets," this is why. First line of every handleSubmit: e.preventDefault().value={name} + onChange={handleChange}. State is the single source of truth; the input just displays it. This is called a controlled component, and it's the foundation of the whole React forms tutorial.Keyboard Events: onKeyDown and Enter-to-Submit
Keyboard events level up your UX: Enter to add an item, Escape to close a modal, arrow keys to navigate a dropdown. The workhorse is onKeyDown, and the property you check is e.key — a readable string like "Enter", "Escape", or "ArrowDown".
Type a skill and press Enter — no form, no button, just onKeyDown checking e.key === "Enter". Notice the guard clauses: bail out early on the wrong key, an empty draft, or a duplicate tag. Early returns keep handlers flat and readable.
e.keyCode is deprecated. Old tutorials check e.keyCode === 13 for Enter. Use e.key === "Enter" instead — readable, standard, and it works with international keyboards. Also prefer onKeyDown over onKeyPress, which is deprecated and doesn't fire for keys like Escape or the arrows.Mouse & Focus Events: Hover, Focus, and Blur
Beyond clicks, two event pairs power a huge amount of everyday UI polish:
onMouseEnter/onMouseLeave— fire once when the pointer crosses the element's boundary. Tooltips, hover cards, preview states.onFocus/onBlur— fire when an element gains or loses keyboard focus. The classic use: validate a field when the user leaves it, not on every keystroke.
🖱️ Hover over this card
Input status: not focused (onBlur fired)
hover:bg-blue-50 in Tailwind) is cheaper — no re-render at all. Reach for onMouseEnter when hover must change state or content: showing a preview, prefetching data, rendering a tooltip component.Event Propagation: Bubbling and stopPropagation
When you click an element, the event doesn't stop there — it bubbles up through every ancestor, firing their handlers on the way. Click a button inside a card inside a page, and any onClick on the button, the card, and the page will all run, innermost first.
See it yourself: click the inner box below and watch the log — both handlers fire, inner first, then outer. Then tick the checkbox to call e.stopPropagation() and click again: the bubble stops at the inner box.
🟦 Outer box (has onClick)
🟨 Inner box (has onClick) — click me!
Event log:
(empty — click the inner box and watch the order)
Where you meet this in real life: a "delete" button inside a clickable list row. Clicking delete also triggers the row's "open" handler — unless the delete handler calls e.stopPropagation().
export function ListRow({ item, onOpen, onDelete }: ListRowProps) {
return (
<li onClick={() => onOpen(item.id)}>
{item.title}
<button
onClick={(e) => {
e.stopPropagation(); // don't let the click reach the row's onClick
onDelete(item.id);
}}
>
🗑 Delete
</button>
</li>
);
}onClick. It attaches one listener at your app's root, lets native events bubble up to it, then figures out which of your components' handlers to run, in the right order. That's why a thousand-row list with an onClick per row costs almost nothing in listeners — React is already doing event delegation for you.Events + State Together: A Star Rating Widget
Time to combine everything into one real component. A star rating widget uses two event types driving two pieces of state: onClick locks in the rating, while onMouseEnter / onMouseLeave drive a temporary hover preview.
How was this tutorial so far?
Hover to preview, click to lock in your rating
Hover across the stars (preview updates live), then click one (the rating sticks even after the mouse leaves). Open the Code tab and study the shown = preview || rating line — the entire "hover overrides saved rating" behavior is that one expression. Konting state, tamang events, malinis na UX.
() => setRating(star)), mouse events for transient state, click events for persistent state, and stable keys on the mapped buttons. Every interactive widget you build from here is a remix of these moves.Common Mistakes
// ❌ BAD: runs during render; if it sets state → infinite loop crash
<button onClick={handleDelete(item.id)}>Delete</button>
// ✅ GOOD: pass the function itself, or an arrow wrapper for arguments
<button onClick={handleReset}>Reset</button>
<button onClick={() => handleDelete(item.id)}>Delete</button>// ❌ BAD: all three calls read the SAME count from this render.
// If count is 0, each line computes 0 + 1 → final result is 1, not 3.
const handleTripleClick = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
// ✅ GOOD: the updater form receives the latest value each time → 3
const handleTripleClick = () => {
setCount((c) => c + 1);
setCount((c) => c + 1);
setCount((c) => c + 1);
};setCount(count + 1) twice uses the same snapshot twice. Habit to build: whenever the next state depends on the previous state, use the updater form setCount((c) => c + 1).// ❌ BAD: the page reloads on submit and your state is gone
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
saveUser(name);
};
// ✅ GOOD: cancel the browser default first
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
saveUser(name);
};// ❌ OUTDATED: the old class-components way — you had to bind `this`
class Counter extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this); // 2018 called...
}
handleClick() {
this.setState({ count: this.state.count + 1 });
}
}
// ✅ MODERN: function components have no `this` to bind. Ever.
export function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => setCount((c) => c + 1);
return <button onClick={handleClick}>{count}</button>;
}If a tutorial tells you to .bind(this) in a constructor, it's teaching the pre-hooks class era. In function components, this-binding simply doesn't exist as a problem — one more reason the ecosystem moved on.
Best Practices + Event Prop Reference
onClick={handleClick} or onClick={() => handleClick(arg)} — never onClick={handleClick()}handleX and keep them small — extract real logic into named functionse.preventDefault() first line of every handleSubmitsetCount((c) => c + 1) whenever next state depends on previous statee.key, not e.keyCode — and onKeyDown, not the deprecated onKeyPressstopPropagation() sparingly — only for real conflicts like buttons inside clickable rowsYour scannable reference — the event props you will actually use, week one to week fifty:
| Event prop | Fires when... | Typical use |
|---|---|---|
onClick | Element is clicked (or tapped) | Buttons, toggles, selections |
onChange | Input value changes (every keystroke) | Controlled inputs, selects, checkboxes |
onSubmit | Form is submitted (button or Enter) | Forms — always with preventDefault() |
onKeyDown | A key is pressed down | Enter-to-add, Escape-to-close, shortcuts |
onMouseEnter / onMouseLeave | Pointer crosses the element boundary | Tooltips, hover previews, prefetching |
onFocus / onBlur | Element gains / loses focus | Validate on blur, focus styling |
onDoubleClick | Element is double-clicked | Edit-in-place, zoom |
onCopy / onPaste | User copies / pastes | Attribution notes, sanitizing pasted text |
onScroll | Element scrolls | Infinite lists, scroll-position UI |
Practice Project: Emoji Reaction Bar
Cement everything with one small build — an emoji reaction bar like the ones under a Facebook post:
- Render 4 reaction buttons (👍 ❤️ 😂 😮) from an array —
onClick={() => react(emoji)}with the arrow-wrapper pattern - Each click increments that emoji's count — store counts in one state object, update immutably
onMouseEntershows a label ("Haha", "Wow") above the hovered button;onMouseLeavehides it- Add a comment input:
onChangetracks the draft,onKeyDownsubmits on Enter, and the whole thing lives in a form withpreventDefault - Stretch goal: put the bar inside a clickable card and use
stopPropagation()so reacting doesn't open the card
If you can build that without peeking back, event handling is officially in your muscle memory. 💪
What's Next?
- React Forms — controlled inputs at scale: multiple fields, validation, checkboxes, selects, and submit flows
- useState Deep Dive — the other half of every handler: updater functions, objects and arrays in state, lazy initialization
- Lists & Keys — render collections and wire per-item handlers (delete, edit, toggle) the right way
Keep practicing! 💪 Events are where React stops being pictures on a screen and starts being an app. Once event → state → render feels automatic, everything else in React — forms, effects, context — is just variations on that loop.