Lesson 4 • Beginner
Handling Events ⚛️
By the end of this lesson you'll be able to make buttons, inputs, and forms do something — wiring up onClick, onChange, and onSubmit handlers, reading the event object, and stopping the page from reloading.
What You'll Learn
- Attach handlers with onClick, onChange, and onSubmit
- Write event handler functions and pass them by name
- Pass arguments to a handler using an arrow wrapper
- Read the synthetic event object (e.target.value, e.type)
- Use e.preventDefault() to stop a form reloading the page
- Avoid the #1 trap: calling the handler instead of passing it
useState, since most event handlers update state. The Try it Yourself editors below run plain JavaScript, so you'll run the handler logic directly and see the output — the JSX wiring is shown in read-only blocks.onClick={ringBell}) and then walk away. You don't press the button yourself during setup — you'd just hear it ring once and never again. You hand React the plan for what to do, and React presses the button (calls your function) every time a visitor actually arrives.1. Click Events & Handler Functions
An event is something the user does — a click, a keypress, a form submit. An event handler is a function you write that runs in response. In React you attach one with a camelCase prop like onClick (not the lowercase onclick from HTML), and you pass the function by name. The golden rule: onClick={handleClick} — no parentheses. The parentheses would call the function immediately; you want to hand the function to React so it can call it on each click.
function LikeButton() {
// 1) Define the handler — a plain function that does the work.
function handleClick() {
alert("You liked this post!");
}
// 2) Pass the function BY NAME — no parentheses.
// React will call handleClick FOR you when the button is clicked.
return <button onClick={handleClick}>Like</button>;
}Read the worked example below. The handleLike function is exactly what you'd pass to onClick — here we call it three times to simulate three clicks and watch the count climb.
Worked example: a click handler
Run it and watch each 'click' call the handler.
// React calls your handler. Here we run the SAME handler logic in plain JS
// so you can see exactly what happens each time a "click" fires.
let likeCount = 0;
// This is the kind of function you'd pass to onClick={...}
function handleLike() {
likeCount = likeCount + 1;
console.log("Liked! Total likes: " + likeCount);
}
// Simulate three clicks. In React, the runtime calls handleLike for you.
handleLike(); // Liked! Total likes: 1
handleLike(); // Liked! Total likes: 2
handleLike();
...The most important detail: pass it, don't call it
This single distinction trips up almost every beginner. Burn it in now:
// ❌ WRONG — handleClick() runs the MOMENT the component renders,
// not when you click. The return value (undefined) becomes the handler.
<button onClick={handleClick()}>Like</button>
// ✅ RIGHT — pass the function itself. React calls it on each click.
<button onClick={handleClick}>Like</button>
// ✅ ALSO RIGHT — wrap it in an arrow function when you need to
// pass an argument (see section 2).
<button onClick={() => handleClick("post-42")}>Like</button>Now you try. Finish the handler and "click" the button by calling it.
🎯 Your turn: write a click handler
Fill in the ___ blanks, then check your output against the expected lines.
// 🎯 YOUR TURN — fill in the blanks marked ___ then press "Try it Yourself".
let count = 0;
// 1) Finish this handler so it adds 1 to count and logs the new total.
function handleIncrement() {
count = ___; // 👉 count + 1
console.log("Count is now: " + count);
}
// In React you would write: <button onClick={handleIncrement}>+1</button>
// Notice: NO parentheses — you pass the function, you don't call it.
// 2) "Click" the button three times by calling the handler
...2. Passing Arguments to Handlers
What if your handler needs data — like which item to delete? You can't write onClick={deletePost(id)} because, as you just learned, that calls deletePost immediately during render. The fix is an arrow wrapper: onClick={() => deletePost(id)}. The arrow function is the handler; it sits and waits, and only calls deletePost(id) when the click actually happens.
{posts.map((id) => (
<button key={id} onClick={() => deletePost(id)}>
Delete #{id}
</button>
))}Worked example: passing an argument
See how the arrow wrapper delays the call until 'click'.
// Passing arguments: wrap the call in an arrow function () => fn(arg).
// The arrow function is the handler; it calls deletePost WHEN clicked.
function deletePost(id) {
console.log("Deleting post #" + id);
}
const posts = [101, 102, 103];
// In JSX you'd write: onClick={() => deletePost(id)}
// Here we build those arrow handlers and call them to prove they work.
posts.forEach((id) => {
const handler = () => deletePost(id); // the wrapper React would store
handler();
...3. The Event Object & onChange
When React calls your handler, it passes one argument: the event object. React calls it a SyntheticEvent — a cross-browser wrapper around the native DOM event, so the same properties work in every browser. The one you'll use constantly is e.target.value: the current text of an input. The onChange event fires on every keystroke, which is how you keep React state in sync with what the user types (a controlled component).
Worked example: reading the event object
See what e.target.value, e.type, and e.target.name give you.
// The event object (React calls it a SyntheticEvent) is passed to your
// handler automatically. It's a cross-browser wrapper around the real
// DOM event, with the same properties everywhere.
// Pretend React handed us this event from typing "Hi" in a text box:
const fakeEvent = {
type: "change",
target: { value: "Hi", name: "username" },
preventDefault() { console.log("(default behaviour prevented)"); },
};
// In an onChange handler you read the typed text from e.target.value:
functio
...In real JSX, that handler wires an input to state like this:
function SearchBar() {
const [query, setQuery] = useState("");
// e.target.value is whatever the user has typed so far.
function handleChange(e) {
setQuery(e.target.value);
}
return (
<input
type="text"
value={query} // controlled: React owns the value
onChange={handleChange} // runs on EVERY keystroke
placeholder="Search..."
/>
);
}4. Forms & preventDefault()
Forms have a built-in browser behaviour: submitting one reloads the page. In a React app that's a disaster — a reload throws away all your state and re-fetches everything. So in your onSubmit handler you call e.preventDefault() as the very first line. That cancels the default reload and lets you handle the submit in JavaScript instead. Put it on <form onSubmit={...}>, not the button — that way pressing Enter inside the form works too.
function SearchForm() {
const [query, setQuery] = useState("");
function handleSubmit(e) {
e.preventDefault(); // ⛔ stop the browser reloading the page
console.log("Searching for:", query);
// ...send the search request here...
}
return (
<form onSubmit={handleSubmit}>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<button type="submit">Search</button>
</form>
);
}Worked example: preventing the default reload
See the order: preventDefault first, then handle the data.
// onSubmit fires when a form is submitted (button click OR Enter key).
// By default the browser RELOADS the page — which wipes your React state.
// e.preventDefault() stops that so YOU handle the submit in JavaScript.
function handleSubmit(e) {
e.preventDefault(); // without this, the page reloads
console.log("Form handled in JS — no reload!");
console.log("Query:", e.target.query);
}
// Simulate the event React would pass to onSubmit:
const submitEvent = {
target: {
...Your turn — complete the submit handler so the page is never reloaded and the typed email is logged.
🎯 Your turn: handle a form submit
Stop the reload and read e.target.value, then check the expected output.
// 🎯 YOUR TURN — complete the form submit handler.
const event = {
target: { value: "alice@example.com" },
preventDefault() { console.log("(page reload stopped)"); },
};
function handleSubmit(e) {
// 1) Stop the browser from reloading the page.
___; // 👉 e.preventDefault()
// 2) Read what the user typed and log it.
const email = ___; // 👉 e.target.value
console.log("Signing up: " + email);
}
handleSubmit(event);
// ✅ Expected output:
//
...Common Errors (and the fix)
- Calling the handler immediately:
onClick={handleClick()}runs on render and fires once (or loops forever if it sets state). Pass it by name:onClick={handleClick}, or wrap it:onClick={() => handleClick(id)}. - "Maximum update depth exceeded": the classic symptom of the bug above —
onClick={setCount(count + 1)}callssetCountduring render, which re-renders, which calls it again. Use an arrow wrapper:onClick={() => setCount(count + 1)}. - The page reloads / state vanishes on submit: you forgot
e.preventDefault(). Make it the first line of youronSubmithandler. - "Cannot read properties of undefined (reading 'value')": your handler reads
e.target.valuebut you forgot to accept the event: writefunction handleChange(e) {… with theeparameter. - "this is undefined" in an old class component: a method passed as
onClick={this.handleClick}loses itsthis. Bind it in the constructor (this.handleClick = this.handleClick.bind(this)) or use a class field arrow. Function components with hooks avoid this entirely — another reason they're now standard.
Pro Tips
- 💡 Name handlers
handleX:handleSubmit,handleDelete,handleNameChange— the convention makes code instantly readable. - 💡 Only add an arrow wrapper when you need an argument.
onClick={handleClick}is cleaner thanonClick={() => handleClick()}when there's no argument. - 💡 preventDefault goes first. Call it at the top of the handler so an early
returncan never skip it. - 💡 Put
onSubmiton the<form>, not the button — then Enter-to-submit works for free.
📋 Quick Reference
| Task | Code |
|---|---|
| Click handler | <button onClick={handleClick}> |
| Pass an argument | onClick={() => del(id)} |
| Read input text | onChange={(e) => set(e.target.value)} |
| Form submit | <form onSubmit={handleSubmit}> |
| Stop page reload | e.preventDefault() |
| Key pressed | e.key === 'Enter' |
| Stop bubbling | e.stopPropagation() |
Frequently Asked Questions
Q: Why is it onClick and not onclick?
JSX uses camelCase for event props, so it's onClick, onChange, onSubmit. It's also a different mechanism from HTML — you pass a real JavaScript function, not a string of code.
Q: When do I need the arrow function () => wrapper?
Only when you need to pass an argument, e.g. onClick={() => deletePost(id)}. If the handler takes no arguments, pass it by name: onClick={handleClick}.
Q: Where does the e in handleChange(e) come from?
React passes it automatically. Whenever your handler is the one React calls directly, the first argument is the SyntheticEvent — just add an e parameter to receive it.
Q: What happens if I forget e.preventDefault() in a form?
The browser does its default thing and reloads the page, which destroys your React state and re-runs the whole app. Always call it first in an onSubmit handler.
Mini-Challenge: A Toggle Handler
No blanks this time — just a brief and an outline. Write the handler yourself, "click" it three times, and match the expected output. This is the exact pattern behind every show/hide, dark-mode, and accordion button you'll ever build.
🎯 Mini-Challenge: build a toggle handler
Write toggle() yourself and call it to simulate clicks.
// 🎯 MINI-CHALLENGE: A toggle handler
//
// Imagine a "Show / Hide" button. You'll write the handler logic.
//
// 1. Create a variable "isOpen" set to false.
// 2. Write a function toggle() that flips isOpen (true -> false -> true)
// and logs: "Panel is now: open" or "Panel is now: closed".
// Hint: isOpen = !isOpen; flips a boolean.
// 3. In React you'd attach it with: <button onClick={toggle}>Toggle</button>
// Here, "click" the button by calling toggle() three times.
//
// ✅ E
...🎉 Lesson Complete
- ✅ Attach handlers with camelCase props:
onClick,onChange,onSubmit - ✅ Pass a function by name —
onClick={fn}, neveronClick={fn()} - ✅ Use an arrow wrapper
() => fn(arg)to pass arguments - ✅ React passes a SyntheticEvent
e; read input text frome.target.value - ✅ Call
e.preventDefault()first inonSubmitto stop the page reloading - ✅ Next lesson: Hooks — deep-dive into
useStateanduseEffect, the engines behind every handler that updates the screen
Sign up for free to track which lessons you've completed and get learning reminders.