Course outline

React / Components and state / Lesson 3

Events and state snapshots

Connect user actions to state updates while reasoning about the values captured by each render.

About 4 min readContent reviewed
On this page

Lesson goals

  • An event is a deliberate boundary
  • Request an update from an interaction
  • Keep event logic distinct from synchronization

An event is a deliberate boundary

Pass a function to an event prop such as onClick rather than calling the function during render. The browser interaction invokes that handler later. This separates describing a button from performing its action. Name handlers for the behavior they own and pass callbacks through props when a parent owns the state. A button should have a meaningful accessible name and an explicit type inside a form. React does not remove ordinary event propagation or default browser behavior; stopPropagation and preventDefault solve different problems and should be used only when the interaction requires them.

State belongs to a render snapshot

useState returns the current render’s value and a setter that requests another render. Calling the setter does not rewrite the variable already captured in that handler. Each render receives its own snapshot, so reading count immediately after setCount can still read the old count from that render. This is not a reason to add arbitrary delays or mutate a local variable. Predict the requested next state and the next committed interface separately. A local ordinary variable is recreated during rendering and does not provide persistent component state across renders.

Request an update from an interaction

The first example increments a counter through a native button. A functional updater computes from the queued previous state and avoids depending on a possibly older count captured by the handler. The updater must be pure because React can evaluate it during its coordination and development checks. Do not send a request or modify another object inside it. The displayed number changes after the update is committed. Tests should click the control and wait for the expected visible result rather than asserting an implementation-specific render count or assuming the setter returns a promise to await.

jsx Component module — requires the course React host
import { useState } from "react";
export default function App() { const [count, setCount] = useState(0); return <main><h1>Counter</h1><p>{count}</p><button type="button" onClick={() => setCount(c => c + 1)}>Add one</button></main>; }
Expected output
Initial: 0. After one Add one click: 1.

The handler queues a pure update from the prior value. One click changes the visible counter from zero to one.

Compose queued updates correctly

The second example queues three functional increments, producing a net increase of three from one click. In contrast, three setCount(count + 1) calls in the same handler all compute from the same captured count and request the same replacement. React batches updates in supported contexts, but batching does not change JavaScript closure semantics. Keep the updater’s input and returned result distinct, and do not read an unrelated state variable inside it to implement a multi-field invariant. A reducer or a single coherent state value can make such transitions clearer when several fields must change together.

jsx Component module — requires the course React host
import { useState } from "react";
export default function App() { const [count, setCount] = useState(0); function addThree() { setCount(c => c + 1); setCount(c => c + 1); setCount(c => c + 1); } return <main><h1>Queued updates</h1><p>{count}</p><button type="button" onClick={addThree}>Add three</button></main>; }
Expected output
Initial: 0. After one Add three click: 3.

Each updater receives the result of the preceding queued update, so one click adds three rather than one.

Keep event logic distinct from synchronization

If an action occurs because a user clicked Purchase, keep the action at that event boundary rather than watching a state flag in an Effect and issuing the purchase indirectly. Effects synchronize external systems with committed state and have different lifecycle behavior. Events can also schedule asynchronous work whose completion needs ownership, cancellation, or stale-result protection. A state snapshot captured before an await remains that snapshot afterward. Test repeated clicks, disabled or pending behavior, and failure outcomes. State updates can represent a pending request, but they cannot by themselves guarantee a remote side effect happened exactly once.

Common errors and fixes

The action runs during rendering

Why it happens
The handler prop contains a call result.
How to fix it
Pass the function or a callback instead.
Prevent it next time
Review event props for unintended invocation.

Three replacement updates add only one

Why it happens
Each uses the same render snapshot.
How to fix it
Use functional updates for repeated prior-state changes.
Prevent it next time
Trace the queued calculations explicitly.

A variable resets after another interaction

Why it happens
It is an ordinary local instead of state.
How to fix it
Use state for values that must persist and affect rendering.
Prevent it next time
Distinguish persistent state from derived local values.

Check your understanding

1. Predict two setCount(count + 1) calls when count is five.

Show worked solution

Both expressions evaluate to six in the same snapshot, so they request replacement with six rather than sequential increments to seven. Two functional updates c => c + 1 would instead compose to seven. The distinction is the queued calculation, not a timer race.

2. Where should a synthetic “save draft” action live when it happens only after clicking Save?

Show worked solution

Keep it in the Save event handler, with explicit pending and failure behavior. Do not create an Effect that sends a save every time a flag or field changes unless continuous synchronization is actually the requirement. This keeps action ownership and user intent visible.

Key takeaways

  • Setters request another render.
  • Handlers capture snapshots.
  • Functional updates compose prior-state changes.

Official references