State and Events
|
This section documents React 19.x for the web, using function components and Hooks throughout (class components appear only where React still requires them, such as error boundaries). React Native is out of scope and gets only a pointer. The React 19 additions follow the current official React documentation. This content was generated with the assistance of AI and should be verified against react.dev before being relied on in production, since React APIs continue to evolve between releases. This section’s bibliography lists the reference material consulted while preparing these pages. |
Interactivity in React comes from two things: event handlers that respond to user input, and state that the component remembers between renders. Changing state re-renders the component.
Event handlers
Attach a handler by passing a function (not calling it) to a camelCased prop such as onClick,
onChange, onSubmit.
function Toolbar() {
function handleSave() {
console.log('saved');
}
return (
<>
<button onClick={handleSave}>Save</button> {/* pass the function */}
<button onClick={() => handleSave()}>Save now</button> {/* inline wrapper */}
<button onClick={handleSave()}>Wrong</button> {/* BUG: calls on render */}
</>
);
}
The handler receives a SyntheticEvent — a cross-browser wrapper over the native event with the same
interface (e.target, e.currentTarget, e.key, e.preventDefault(), e.stopPropagation()). React
attaches one listener at the root and dispatches from there.
function SearchForm({ onSearch }) {
function handleSubmit(e) {
e.preventDefault(); // don't reload the page
const q = new FormData(e.target).get('q');
onSearch(q);
}
return (
<form onSubmit={handleSubmit}>
<input name="q" />
</form>
);
}
Events bubble through the React tree. Use onClickCapture for the capture phase, and e.stopPropagation() to
stop a bubble. For the full capture/target/bubble model see
Events. Reference:
Responding to Events.
useState
useState gives a component a value that survives re-renders plus a setter that schedules a re-render.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // [current value, setter]
const [name, setName] = useState(''); // as many as you need
return (
<button onClick={() => setCount(count + 1)}>
{name || 'anon'}: {count}
</button>
);
}
State is per component instance: render <Counter /> twice and each has its own count. The initial
value is used only on the first render. See
State: A Component’s Memory.
Render and commit
Every screen update is three steps:
-
Trigger — the first render (
createRoot(…).render) or a state update fromsetState. -
Render — React calls your components and works out what the DOM should look like.
-
Commit — React applies the minimal set of DOM changes, then the browser paints.
(initial render or setState)"] --> B["Render
React calls components,
diffs the element tree"] B --> C["Commit
React mutates the DOM
(only what changed)"] C --> D["Browser paints"]
See Render and Commit.
State as a snapshot, batching, and updater functions
A component’s count is fixed for the duration of a render — a snapshot. Calling setCount(count + 1)
three times in one handler still adds only 1, because all three read the same snapshot.
React automatically batches state updates — those in an event handler and, since React 18, those in
promises, setTimeout, and native event handlers too — into a single re-render. To base an update on the
latest pending value, pass an updater function:
setCount(count + 1); // count is the render's snapshot
setCount((c) => c + 1); // c is the latest queued value
setCount((c) => c + 1);
setCount((c) => c + 1); // three updaters -> +3
References: State as a Snapshot, Queueing a Series of State Updates.
Updating objects and arrays
Treat state as immutable. Never mutate an object or array in state; create a new one and pass it to the setter, so React sees a new reference and re-renders.
// object
setUser({ ...user, name: 'Ada' });
// array
setItems([...items, newItem]); // add
setItems(items.filter((i) => i.id !== id)); // remove
setItems(items.map((i) => (i.id === id ? { ...i, done: true } : i))); // update one
For deeply nested state, Immer (useImmer) lets you write mutating-looking
code that produces an immutable update. References:
Updating Objects in State,
Updating Arrays in State.