HomeBytesheetsRoadmapsUI TemplatesUI ToolsStack Discussion
5 Topics
Chapter•80 Minutes

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.

Functional state update
const [count, setCount] = useState(0);

function incrementTwice() {
  setCount(c => c + 1);
  setCount(c => c + 1);
}
💡 Each updater receives the latest queued value, so two increments produce a net increase of two.
Key Takeaways
  • 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.
Knowledge Check

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.

Form submission
function LoginForm() {
  function handleSubmit(event) {
    event.preventDefault();
    // validate and submit credentials
  }

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit">Sign in</button>
    </form>
  );
}
💡 The form owns the submit event. Keeping submission logic in a named handler improves readability and testing.
Key Takeaways
  • 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.

Controlled input
function SearchBox() {
  const [query, setQuery] = useState("");

  return (
    <input
      value={query}
      onChange={e => setQuery(e.target.value)}
      aria-label="Search"
    />
  );
}
💡 The input's value is derived from React state, so every change flows through the component.
Key Takeaways
  • Controlled inputs make form state explicit.
  • Use checked for 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.

Reducer pattern
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 };
  }
}
💡 The reducer centralizes allowed state transitions while the component dispatches semantic actions.
Key Takeaways
  • 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.

Model reality

If two booleans cannot logically be true together, consider replacing them with one discriminated state.

Key Takeaways
  • Explicit states prevent contradictory UI flags.
  • Reducers and discriminated unions are practical state-machine tools.
  • State models should make invalid states difficult to represent.