Handling Events in React
Learn how React's synthetic event system works, how to correctly type event handlers in TypeScript, and how to control event propagation and default browser behavior.
Almost every interactive feature in a React application — a button click, a form submission, a keystroke in a search box, a drag-and-drop reorder — starts with an event handler. React wraps the browser's native events in its own SyntheticEvent system so that handlers behave consistently across browsers, and TypeScript's React type definitions give you precise types for every event category so you get autocomplete and compile-time safety instead of guessing at property names.
The SyntheticEvent system
When you attach a handler like onClick or onChange to a JSX element, React does not attach a native listener directly to that DOM node in the way you might with addEventListener. Instead React registers a single listener near the root of the tree (historically at the document, and in modern React at the root container) and uses event delegation to figure out which component's handler should fire. This is more efficient than attaching thousands of individual listeners and it lets React normalize event behavior across browsers.
The object your handler receives is not the raw browser event — it is a SyntheticEvent, a cross-browser wrapper around the native event with the same interface (stopPropagation, preventDefault, target, currentTarget, and so on). SyntheticEvent objects are pooled and reused in older React versions for performance, though as of React 17+ this pooling was removed, so you can safely read event properties asynchronously in modern React without calling persist().
TypeScript's @types/react package defines a whole family of typed event interfaces: React.MouseEvent, React.ChangeEvent, React.KeyboardEvent, React.FormEvent, React.FocusEvent, React.ClipboardEvent, React.DragEvent, React.PointerEvent, and more. Each is generic over the DOM element type it is attached to, for example React.ChangeEvent<HTMLInputElement>, so that event.target is correctly typed instead of falling back to EventTarget or any.
Typing handlers correctly
When you write an inline arrow function directly inside JSX, such as onClick={(e) => doSomething(e)}, TypeScript can usually infer the event type from the JSX attribute's expected signature, so you rarely need to annotate it by hand. When you extract a handler into a standalone named function outside the JSX, there is no such inferred context, so you must annotate the parameter explicitly using the matching event type and element generic.
Getting the element generic right matters. React.ChangeEvent<HTMLInputElement> gives you event.target.value as a string and event.target.checked as a boolean for checkboxes. React.ChangeEvent<HTMLSelectElement> gives you the selected option's value. Using the wrong element type, or using a loose type like Event or any, either breaks autocomplete or silently allows bugs like reading .value off an element that does not have one.
Different event categories carry different useful properties: MouseEvent carries clientX/clientY and button; KeyboardEvent carries key, code, and modifier flags like shiftKey and ctrlKey; PointerEvent generalizes mouse, touch, and pen input with a pointerType field; ClipboardEvent exposes clipboardData for copy/paste handling; and DragEvent exposes dataTransfer for drag-and-drop interactions.
preventDefault, stopPropagation, and delegation
event.preventDefault() stops the browser's default behavior for that event — for example, stopping a form's onSubmit from causing a full page reload and navigation, or stopping a link's onClick from navigating away. This is one of the most common calls you will write in any form handler.
event.stopPropagation() prevents the event from continuing to bubble up to parent elements' handlers. Because React's own delegation relies on bubbling to a root listener internally, stopPropagation still works correctly for your own component tree's handlers — it just stops React from notifying ancestor handlers you've attached, which is useful for things like a dropdown menu item that shouldn't also trigger the click-outside handler on its container.
It helps to think in terms of event priority as well: React internally categorizes updates triggered from events (like typing, clicking) as discrete, user-blocking work that should be processed promptly, versus updates from background sources. You don't usually configure this directly in typical code, but understanding that user-initiated events are treated as high priority explains why React keeps typing and clicking feeling instantaneous even while other rendering work is in flight.
Typed change handler
The generic parameter <HTMLInputElement> is what makes event.target.value resolve to string instead of unknown. Swap the generic for HTMLTextAreaElement or HTMLSelectElement when attaching the same pattern to those elements.
const handleChange = (
event: React.ChangeEvent<HTMLInputElement>
) => {
setValue(event.target.value);
};Code Examples
The event type is parameterized with HTMLInputElement so event.target.value is correctly typed as string, giving full autocomplete and type checking. Expected result: Typing in the input updates value on every keystroke.
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
This combines a typed form submit handler that calls preventDefault to stop the page reload, a keyboard handler checking event.key, and a click handler that stops propagation so it doesn't trigger an outer container's click listener. Notice each handler uses the event type matching the element it's attached to: FormEvent<HTMLFormElement> for the form, KeyboardEvent<HTMLInputElement> for the input, and MouseEvent<HTMLButtonElement> for the button.
Common mistake: Forgetting event.preventDefault() in the submit handler would cause the browser to reload the page on every form submission, wiping out React's state.
function SearchBox() {
const [value, setValue] = useState("");
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
};
return <input value={value} onChange={handleChange} placeholder="Search..." />;
}function ContactForm() {
const [email, setEmail] = useState("");
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log("Submitting:", email);
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Escape") {
setEmail("");
}
};
const handleMenuClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
console.log("Menu button clicked, won't bubble to parent");
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
onKeyDown={handleKeyDown}
/>
<button type="submit" onClick={handleMenuClick}>Send</button>
</form>
);
}Event delegation flow
React listens once near the root and dispatches synthetic events down to the right component handlers.
User clicks <button>
|
v
Native DOM event fires
|
v
React's root listener catches it
|
v
React builds a SyntheticEvent
|
v
React walks the virtual tree and
calls matching onClick handlers,
innermost first (bubbling)Important Rules
Match the generic event type's element parameter to the actual DOM element the handler is attached to.
Call event.preventDefault() in form submit handlers to stop the default full-page reload/navigation.
Use event.stopPropagation() only when you deliberately want to block an event from reaching parent handlers.
Prefer inline handlers for simple one-line logic; extract named, explicitly-typed handlers once logic grows beyond a line or two.
Never type an event parameter as any — use the specific React.*Event<ElementType> that matches the DOM element and event kind.
Where this shows up
Every form input, search box, modal dialog, dropdown menu, and drag-and-drop list in a production app relies on correctly typed and carefully controlled event handlers. A checkout form that doesn't call preventDefault reloads the page and loses the customer's cart data; a dropdown menu that doesn't call stopPropagation might immediately close itself because its own click bubbled up to a document-level 'click outside to close' listener.
const handleChange = (event: any) => setValue(event.target.value);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => setValue(event.target.value);
Using any silently discards all type checking on the event object, so typos like event.traget.value or reading a property that doesn't exist on that element would not be caught until runtime.
<form onSubmit={handleSubmit}> // handleSubmit never calls event.preventDefault() const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); // submit logic };
Without preventDefault, submitting the form triggers a full browser navigation/reload, discarding all component state and any in-flight async work.
[multiple-choice] Which type correctly types an onChange handler for a text input? A. React.ChangeEvent<HTMLInputElement> B. React.MouseEvent<HTMLInputElement> C. Event D. any
A. React.ChangeEvent<HTMLInputElement> — ChangeEvent<HTMLInputElement> is the correct generic event type for an input's onChange, giving event.target.value the string type.
[multiple-choice] What does calling event.preventDefault() inside a form's onSubmit handler accomplish? A. It stops the browser's default full-page reload/navigation on submit B. It stops the event from bubbling to parent elements C. It clears all form field values D. It cancels any pending async requests
A. It stops the browser's default full-page reload/navigation on submit — preventDefault suppresses the browser's default action for that event type — for a form submit, that default action is navigating/reloading the page.
[true-false] True or false: React attaches a separate native event listener to every single DOM element that has an onClick prop. A. True B. False
B. False — False. React uses event delegation, registering listeners near the root of the tree and dispatching SyntheticEvent objects to the correct handlers, rather than attaching one native listener per element.
Write a typed handleSubmit function for a <form onSubmit={...}> that calls event.preventDefault(), and a typed handleKeyDown for an <input> that logs the pressed key.
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); }; const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => { console.log(event.key); };
The event type for form submission is React.FormEvent<HTMLFormElement>; the event type for a keydown on an input is React.KeyboardEvent<HTMLInputElement>, and the key pressed is available as event.key.