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:

  1. Trigger — the first render (createRoot(…​).render) or a state update from setState.

  2. Render — React calls your components and works out what the DOM should look like.

  3. Commit — React applies the minimal set of DOM changes, then the browser paints.

flowchart LR A["Trigger
(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"]

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

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.