State, Events, Forms & User Interaction
Master local state, functional updates, event handling, controlled inputs, validation, reducers, and state-machine thinking.
useState: Updates, Batching & Functional State
useState gives a component local state. Calling its setter schedules a state update; it does not synchronously mutate the variable from the current render.
When a new state depends on the previous state, use the functional updater form. This is especially important when multiple updates can be batched or when callbacks capture an older render.
State should represent information that can change and that affects rendering. Avoid storing values that can be computed cheaply from existing props or state.
const [count, setCount] = useState(0);
function incrementTwice() {
setCount(c => c + 1);
setCount(c => c + 1);
}- State setters schedule a new render; they do not mutate the current render's variable.
- Use functional updates when next state depends on previous state.
- Keep derived values out of state when possible.
Why use `setCount(c => c + 1)` instead of `setCount(count + 1)` when chaining updates?
Synthetic Events & Event Handling
React event handlers are functions passed to event props such as onClick, onChange, and onSubmit. Pass a function reference; do not invoke it during render unless you intentionally want the return value.
Event handlers are a natural place for user-driven side effects: updating state, calling an API, starting a transition, or navigating. Keep the event handler focused on the event and delegate substantial business logic to reusable functions.
For forms, call preventDefault() when you are intentionally handling submission without a native navigation.
function LoginForm() {
function handleSubmit(event) {
event.preventDefault();
// validate and submit credentials
}
return (
<form onSubmit={handleSubmit}>
<button type="submit">Sign in</button>
</form>
);
}- Event handlers are explicit side-effect boundaries.
- Do not call event handlers during render unless that is actually intended.
- Use semantic HTML events such as form submit.
Controlled Inputs & Form State
A controlled input receives its displayed value from React state and reports changes through an event handler. This gives React a single source of truth and makes validation, conditional UI, and submission logic straightforward.
Controlled inputs are not automatically superior for every field. Very large forms can require careful optimization or specialized form libraries. The principle is to choose an ownership model deliberately rather than mixing uncontrolled DOM state and React state accidentally.
Checkboxes use checked rather than value for their boolean UI state.
function SearchBox() {
const [query, setQuery] = useState("");
return (
<input
value={query}
onChange={e => setQuery(e.target.value)}
aria-label="Search"
/>
);
}- Controlled inputs make form state explicit.
- Use
checkedfor controlled checkbox state. - Keep validation separate from presentation when forms become complex.
useReducer & Complex State Transitions
useReducer is useful when a component has many related state transitions. Instead of exposing many setters, you describe events as actions and centralize state transitions in a reducer.
A reducer should be pure: given the same state and action, it returns the same next state. Side effects such as network requests belong outside the reducer.
Reducers become especially valuable when transitions have domain meaning: cart/itemAdded, checkout/submitted, or editor/saved. This produces a more explicit state model than a collection of loosely related booleans.
type State = { count: number };
type Action =
| { type: "increment" }
| { type: "decrement" };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "increment": return { count: state.count + 1 };
case "decrement": return { count: state.count - 1 };
}
}- Reducers model transitions, not side effects.
- Use actions that describe what happened.
- Discriminated action types work especially well with TypeScript.
State Machines & Impossible States
Many UI bugs come from combinations of booleans that should never coexist. For example, isLoading, isSuccess, and hasError can accidentally represent contradictory states.
Instead, model mutually exclusive states explicitly: idle, loading, success, and error. This reduces the number of invalid combinations and makes rendering exhaustive.
You do not need a state-machine library to benefit from state-machine thinking. TypeScript unions, reducers, and explicit transition functions are often enough.
If two booleans cannot logically be true together, consider replacing them with one discriminated state.
- Explicit states prevent contradictory UI flags.
- Reducers and discriminated unions are practical state-machine tools.
- State models should make invalid states difficult to represent.