Event handling
React's synthetic event system, writing event handlers, passing arguments, and the common traps. The foundation for Chapter 19 (typing events) and Chapter 27 (Server Actions).
In Chapter 5, while learning state and useState, we casually used an event handler called onClick. It worked, but there are a few things worth knowing about how React handles events. This chapter covers them properly.
The event model from this chapter gets solidified with TypeScript in Chapter 19 (Typing events and forms), and extends one more time into the new model (<form action={fn}>) in Chapter 27 (Server Actions and forms). Lock down the basic patterns of this chapter and the later chapters read lightly.
How to handle events in React #
In React, you handle events by passing a handler function via a JSX attribute. The only difference from HTML is that you use the camelCase onClick instead of HTML’s onclick.
function App() {
function handleClick() {
alert('The button was clicked!');
}
return <button onClick={handleClick}>Click</button>;
}
export default App;There is one common mistake here. Do not call the function (handleClick()) — pass the function itself (handleClick).
<button onClick={handleClick()}>Click</button>Written this way, the moment the component renders, handleClick() runs and the alert pops up. On top of that, onClick gets registered with handleClick’s return value (undefined), so an actual click does nothing.
<button onClick={handleClick}>Click</button>Pass the reference; React calls it at click time. Remember this difference.
Inline handlers #
For simple handlers, it is common to write an arrow function directly inside the JSX.
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(prev => prev + 1)}>
Count: {count}
</button>
);
}() => setCount(prev => prev + 1) is an anonymous function called at click time. For simple one-line handlers, inline is convenient; when the logic grows, splitting it into a separate function reads better. There is no fixed rule — follow your team or personal preference.
Passing arguments to a function #
When you need to pass arguments to the handler function, wrap it with an inline arrow function.
function App() {
function handleClick(name) {
alert(`Hello, ${name}!`);
}
return (
<>
<button onClick={() => handleClick('Cheolsu')}>Greet Cheolsu</button>
<button onClick={() => handleClick('Younghee')}>Greet Younghee</button>
</>
);
}Do not write:
<button onClick={handleClick('Cheolsu')}>...</button>As we saw above, that invokes immediately on render. To pass arguments you must wrap with an arrow function once, expressing “call this at click time.”
The event object #
Event handlers receive an event object as their first parameter. That object carries information about which element fired which event.
import { useState } from 'react';
function InputDemo() {
const [text, setText] = useState('');
function handleChange(e) {
setText(e.target.value);
}
return (
<div>
<input type="text" value={text} onChange={handleChange} />
<p>Input: {text}</p>
</div>
);
}
export default InputDemo;The parameter is conventionally named e or event. e.target is the DOM element the event fired on, and you pull the input value out via e.target.value.
React’s event object is, strictly speaking, not the browser’s native event but a SyntheticEvent. React wraps it so it behaves identically across browsers. The API is nearly the same as native events, so day to day you do not have to worry about it. The familiar properties / methods like e.preventDefault(), e.target, and e.key work as expected.
Chapter 19 (Typing events and forms) covers the precise types for this synthetic event — ChangeEvent<HTMLInputElement>, FormEvent, KeyboardEvent — and there we also discuss the type difference between e.target and e.currentTarget.
Common events #
The most frequently used event handlers:
onClick— clickonChange— when the value of an input element (input, textarea, select) changesonSubmit— when a form is submittedonKeyDown/onKeyUp— when a key is pressed or releasedonMouseEnter/onMouseLeave— when the mouse enters or leaves an elementonFocus/onBlur— focus enter / leave
Each event carries the information appropriate to it in the event object. For onChange, look at e.target.value; for onKeyDown, look at e.key (the name of the pressed key).
function SearchBox() {
function handleKeyDown(e) {
if (e.key === 'Enter') {
alert('The Enter key was pressed');
}
}
return <input type="text" onKeyDown={handleKeyDown} />;
}Preventing default behavior #
The browser has default behavior for some events. Submitting a form reloads the page; clicking a link navigates. To block that default, call the event object’s preventDefault().
import { useState } from 'react';
function LoginForm() {
const [email, setEmail] = useState('');
function handleSubmit(e) {
e.preventDefault(); // block the page reload that comes with form submission
console.log('Submitted email:', email);
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">Log in</button>
</form>
);
}
export default LoginForm;A form fires onSubmit automatically when the submit button is pressed or Enter is hit in an input. Without e.preventDefault(), the browser reloads the page and the handler logic we wrote loses its meaning.
Chapter 27 (Server Actions and forms) covers the new model where this preventDefault() is no longer needed. Inside <form action={serverFn}> the page does not reload and the server function runs instead. Keep in mind that the pattern in this chapter feeds naturally into that model.
Passing event handlers as props #
Event handlers are just functions, so they can be passed down to child components as props. This pattern is used very often when the parent needs to handle events that originate in the child.
function Button({ label, onClick }) {
return (
<button onClick={onClick} style={{ padding: '8px 16px' }}>
{label}
</button>
);
}
export default Button;import Button from './Button';
function App() {
function handleSave() {
alert('Saved');
}
function handleCancel() {
alert('Cancelled');
}
return (
<>
<Button label="Save" onClick={handleSave} />
<Button label="Cancel" onClick={handleCancel} />
</>
);
}The parent passes “what to do” (the handler), and the child reports “when that thing happens” (the click). Handler prop names are conventionally prefixed with on (onClick, onSave, onItemSelect, and so on).
This “child event → parent handler” pattern becomes the key tool of Chapter 11 (Lifting state up).
Updating state inside a handler #
A pattern from Chapter 5: changing state inside an event handler is the most common pattern of all.
import { useState } from 'react';
function Toggle() {
const [isOn, setIsOn] = useState(false);
function handleToggle() {
setIsOn(prev => !prev);
}
return (
<div>
<p>Current state: {isOn ? 'ON' : 'OFF'}</p>
<button onClick={handleToggle}>Toggle</button>
</div>
);
}
export default Toggle;When the event fires → the handler runs → state updates → the screen re-draws. This is the most fundamental pattern of a React app.
Try it yourself #
Let us build a simple input form. The user types a name and a message, presses “Add,” and the message appears below the form. Chapters 7 and 8 extend this further.
Create src/MessageForm.jsx.
import { useState } from 'react';
function MessageForm() {
const [name, setName] = useState('');
const [message, setMessage] = useState('');
const [lastSubmitted, setLastSubmitted] = useState(null);
function handleSubmit(e) {
e.preventDefault();
if (!name || !message) return;
setLastSubmitted({ name, message });
setName('');
setMessage('');
}
return (
<div style={{ padding: '16px', border: '1px solid #ccc', borderRadius: '8px' }}>
<form onSubmit={handleSubmit}>
<div>
<input
type="text"
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div style={{ marginTop: '8px' }}>
<input
type="text"
placeholder="Message"
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
</div>
<button type="submit" style={{ marginTop: '8px' }}>Add</button>
</form>
{lastSubmitted && (
<p style={{ marginTop: '12px' }}>
Last submission: <strong>{lastSubmitted.name}</strong>. {lastSubmitted.message}
</p>
)}
</div>
);
}
export default MessageForm;Wire it up in src/App.jsx.
import MessageForm from './MessageForm';
function App() {
return (
<>
<h1>Message form</h1>
<MessageForm />
</>
);
}
export default App;Type in a name and a message and hit Enter or the “Add” button. The last submission shows up below, and the input fields are cleared. Try removing e.preventDefault() and watch the form reload the page and erase your input.
lastSubmitted && (...) part in the code above might look new — that is conditional rendering. Show it if there is a value, hide it if not. Chapter 7 covers it in detail.Exercises #
- Add a “Reset” button to
MessageFormabove so that one press resetslastSubmittedback tonull.<button type="button" onClick={() => setLastSubmitted(null)}>Reset</button>. Note: without explicitly settingtype="button", it gets treated as a submit button inside a form. - Practice keyboard events. Create
src/SearchBox.jsxand attach anonKeyDownhandler to the<input>so that pressing Enter pops up the current input value viaalert. Use theif (e.key === 'Enter') alert(...)pattern. - Practice passing a handler as a prop. Build a
Buttoncomponent that takeslabelandonClickas props, then use the sameButtonthree times in the parent, passing a different handler to each. On click, each should log a different message to the console.
In one line: Register handlers with camelCase attributes like
onClick. Pass the function, don’t call it ({handleClick}, not{handleClick()}). To pass arguments, wrap with an arrow function. Handlers receive a synthetic event objecteas the first parameter. You can block default browser behavior withe.preventDefault(). Handlers can be passed down to children as props (names start withon).
Next chapter #
The {lastSubmitted && ...} pattern we glimpsed in the MessageForm example leads directly into the next topic. In the next chapter, Chapter 7: Conditional rendering, we sort out the various patterns for showing or hiding parts of the screen, or changing their appearance, based on state.