Events
React events in depth: attaching handlers, passing vs calling functions, the SyntheticEvent wrapper and event delegation, preventDefault and stopPropagation, controlled inputs and forms, passing arguments to handlers, and how React's system differs from native DOM listeners.
What events are for. Events let components respond to user interaction — clicks, typing, submitting, hovering, key presses. In React you attach handlers declaratively with camelCase props like onClick, onChange, onSubmit, onKeyDown, passing a function reference: <button onClick={handleClick}>. You describe which function handles the event; React wires up the actual listening for you.
Pass the function, don't call it (the #1 bug). onClick={handleClick} passes the function so React calls it on click. onClick={handleClick()} calls it immediately during render and uses the return value as the handler — usually a bug that fires on every render. To pass arguments, wrap it in an arrow: onClick={() => handleClick(id)} — now you're passing a new function that calls yours with the argument when clicked.
SyntheticEvent — a cross-browser wrapper. React doesn't hand you the raw browser event; it wraps it in a SyntheticEvent that normalizes differences across browsers and exposes a consistent API (e.target, e.preventDefault(), e.stopPropagation(), e.currentTarget). This gives uniform behavior everywhere. If you ever need the underlying browser event, it's available as e.nativeEvent.
Event delegation under the hood. React does not attach a listener to every DOM node. Instead it uses event delegation: it attaches listeners at the root container and lets events bubble up, then dispatches them to the right handler. In React 17+ these root listeners are attached to the app's root container (not document), which improves interoperability when embedding React in other apps. You get per-element handlers in your code, but far fewer real DOM listeners.
preventDefault — stop the browser's default action. Some events have built-in browser behavior: submitting a form reloads/navigates the page, clicking a link navigates, checkboxes toggle. Call e.preventDefault() to cancel that default so you can handle it in JavaScript instead — the classic case is a form's onSubmit in a single-page app, where you preventDefault() then update state or fetch.
stopPropagation — stop the bubble. Events bubble from the target up through ancestors, so a click on a button inside a card triggers both the button's and the card's onClick. Call e.stopPropagation() in the inner handler to prevent the event from reaching outer handlers. preventDefault and stopPropagation are independent: one cancels the default action, the other stops propagation.
Controlled inputs — React owns the value. A controlled input has its value driven by state and an onChange that updates that state: <input value={x} onChange={e => setX(e.target.value)} />. React is the single source of truth, which enables validation, formatting, conditional disabling, and always-consistent UI. This loop — state → value → onChange → setState → re-render — is the foundation of React forms.
Uncontrolled inputs — the DOM owns the value. An uncontrolled input keeps its own value in the DOM; you read it via a ref (or from the form on submit) rather than binding it to state. It's simpler for basic forms and integrates with non-React code, but you lose React's live control. Rule of thumb: prefer controlled for anything with validation or dynamic behavior; uncontrolled is fine for simple, fire-and-forget inputs.
Forms and onSubmit. Handle submission on the <form>'s onSubmit, not the button's onClick, so keyboard Enter also works. Call e.preventDefault() first, then read values (from state for controlled inputs, or from e.target/refs otherwise) and do the work — update state, validate, or make a request. Give the submit button type="submit".
Handler definition styles. Handlers are just functions. Inline arrows (onClick={() => approve(id)}) are convenient for passing arguments and for tiny logic. Named handlers defined in the component body keep complex logic readable and are easier to test. A minor performance note: inline arrows create a new function each render, which usually doesn't matter unless you're passing them to heavily memoized children (then reach for useCallback).
Differences from native DOM events. Beyond delegation and SyntheticEvent: React uses camelCase (onClick, not lowercase onclick), you pass functions not strings, and returning false from a handler does not prevent default (you must call e.preventDefault()). Since React 17, SyntheticEvents are no longer pooled, so you can safely access the event asynchronously without calling e.persist().
The mental model (memorise this). Attach camelCase handlers by passing a function reference (wrap in an arrow to pass args). React gives you a normalized SyntheticEvent and uses root-level delegation. Call preventDefault() to cancel the browser's default (e.g. form reload) and stopPropagation() to stop bubbling. Bind input value to state with onChange for controlled inputs, and handle forms on onSubmit.
React's event system is a dispatcher/front-controller: instead of registering a listener on every widget, it registers once at the root and routes bubbling events to the right handler — like a single Vert.x router or a Spring DispatcherServlet fanning requests out to the correct controller method rather than one server socket per endpoint. The SyntheticEvent is an adapter/normalized request object shielding you from browser (transport) quirks, and e.nativeEvent is the raw underlying request if you need it. preventDefault() is short-circuiting the framework's default handling (like returning early before the default view renders), and stopPropagation() is halting a filter/middleware chain so upstream interceptors don't also run. A controlled input is a two-way-bound field where your model is the single source of truth and every keystroke is a state transition — much like validating and echoing form fields through the server model rather than trusting the client's own copy.
- Attach handlers with camelCase props (onClick, onChange, onSubmit) and pass a function reference, not a call.
- onClick={fn} passes the function; onClick={fn()} calls it during render (a bug) — use onClick={() => fn(arg)} to pass arguments.
- Handlers receive a SyntheticEvent: a cross-browser wrapper with e.target, e.preventDefault, e.stopPropagation; raw event is e.nativeEvent.
- React uses event delegation at the root container (since v17), not one listener per node.
- e.preventDefault() cancels the browser default (form reload, link navigation, etc.).
- e.stopPropagation() stops the event bubbling to ancestor handlers; it's independent of preventDefault.
- Controlled input: value bound to state + onChange updating it — React is the single source of truth.
- Uncontrolled input: DOM holds the value, read via ref; simpler but less control — prefer controlled for validation.
- Handle form submission on <form> onSubmit (so Enter works), calling preventDefault first.
- React events are camelCase functions (not string onclick), returning false doesn't prevent default, and events aren't pooled since v17.
Worked Code
function Toolbar({ id }: { id: number }) {
const handleClick = () => console.log('clicked');
const approve = (itemId: number) => console.log('approve', itemId);
return (
<>
{/* ✅ pass the function reference */}
<button onClick={handleClick}>OK</button>
{/* ❌ calls immediately during render, sets return value as handler */}
{/* <button onClick={handleClick()}>Bug</button> */}
{/* ✅ arrow wrapper to pass an argument on click */}
<button onClick={() => approve(id)}>Approve</button>
</>
);
}import { useState } from 'react';
function ExpenseForm() {
const [amount, setAmount] = useState<number>(0);
const [note, setNote] = useState('');
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); // stop the browser's full-page reload
console.log({ amount, note }); // use the controlled state values
setAmount(0); setNote(''); // reset the form via state
};
return (
<form onSubmit={handleSubmit}>
{/* controlled: value from state, onChange writes back to state */}
<input type="number" value={amount}
onChange={e => setAmount(Number(e.target.value))} />
<input type="text" value={note}
onChange={e => setNote(e.target.value)} />
<button type="submit">Add Expense</button>
</form>
);
}function Card() {
const openCard = () => console.log('card clicked');
const deleteItem = (e: React.MouseEvent) => {
e.stopPropagation(); // don't let the click bubble to the card's onClick
console.log('deleted (card did NOT open)');
};
return (
<div onClick={openCard} className="card">
<p>Click the card to open it.</p>
{/* Without stopPropagation, clicking Delete would ALSO open the card */}
<button onClick={deleteItem}>Delete</button>
{/* preventDefault example: cancel link navigation */}
<a href="/somewhere" onClick={e => e.preventDefault()}>Won't navigate</a>
</div>
);
}import { useState, useRef } from 'react';
// CONTROLLED: React state is the source of truth (validate/format live).
function Controlled() {
const [value, setValue] = useState('');
return <input value={value} onChange={e => setValue(e.target.value)} />;
}
// UNCONTROLLED: the DOM keeps the value; read it via a ref when needed.
function Uncontrolled() {
const ref = useRef<HTMLInputElement>(null);
const read = () => console.log(ref.current?.value); // read on demand
return (
<>
<input ref={ref} defaultValue="" /> {/* defaultValue, not value */}
<button onClick={read}>Read</button>
</>
);
}▶Try It Live
Edit the code and press Run — it executes safely in a sandboxed iframe. Use the Console tab for log output.
Interview-Ready Q&A
onClick={fn} passes the function reference so React invokes it on click. onClick={fn()} calls fn immediately during render and assigns its return value as the handler — usually a bug that also fires every render. To pass arguments, wrap it: onClick={() => fn(id)}, which creates a function that calls fn with the argument when the click occurs.
- 1Attach handlers with camelCase props: onClick, onChange, onSubmit, onKeyDown.
- 2Pass the function reference; wrap in an arrow to pass arguments — never call it in JSX.
- 3Handlers get a SyntheticEvent (cross-browser); raw event is e.nativeEvent.
- 4React delegates events at the root container (since v17) rather than per-node listeners.
- 5preventDefault() cancels the default action (e.g. form reload); stopPropagation() stops bubbling.
- 6Controlled input: value bound to state + onChange — React is the source of truth.
- 7Uncontrolled input: DOM owns the value, read via ref, initialize with defaultValue.
- 8Handle submission on <form> onSubmit so Enter works; give the button type="submit".
- 9Returning false from a handler does NOT prevent default — call e.preventDefault().
- 10Inline arrows are fine; reach for useCallback only when passing to memoized children.