Skip to content

React State, Events, and Effects ​

A React component's render is a snapshot. Props and state values belong to that particular render, and the returned JSX describes what the interface should look like for them.

jsx
function Counter() {
  const [count, setCount] = useState(0);

  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

Calling setCount requests another render. It does not mutate the count constant already captured by the current event handler.

State updates are queued ​

jsx
function incrementThreeTimes() {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
}

Each call uses the same snapshot, so this requests count + 1, not three increments. When the next value depends on the previous queued value, use an updater:

jsx
setCount(value => value + 1);
setCount(value => value + 1);
setCount(value => value + 1);

Objects and arrays in state should be replaced rather than mutated:

jsx
setTransactions(current =>
  current.map(transaction =>
    transaction.id === id
      ? { ...transaction, settled: true }
      : transaction,
  ),
);

React uses identity to detect changes and may retain old snapshots. Mutation makes those snapshots inconsistent and can prevent rendering.

Store the minimum state ​

If a value can be calculated from current props and state during rendering, do not synchronize another state variable for it:

jsx
const visibleTransactions = transactions.filter(transaction =>
  transaction.description.toLowerCase().includes(query.toLowerCase()),
);

Storing both transactions and visibleTransactions creates two sources of truth. Use memoization only when calculation is observably expensive or stable identity is required; useMemo is a performance tool, not a semantic requirement.

State belongs at the nearest common owner of components that need to read or change it. Pass values down and event callbacks up:

jsx
function TransactionRow({ transaction, onSettle }) {
  return (
    <button onClick={() => onSettle(transaction.id)}>
      Settle {transaction.description}
    </button>
  );
}

This “lifting state up” keeps ownership explicit. Context helps with widely shared values such as theme or session, but placing all changing application state in one context can cause broad coupling and rendering.

Events are where user-driven side effects begin ​

Pass a function as a handler; do not call it while rendering:

jsx
<>
  <button onClick={save}>Save</button>
  <button onClick={() => save(transaction.id)}>Save</button>
</>

onClick={save()} runs immediately and passes its result as the handler.

Event handlers capture the render in which they were created. This is normally useful. Async handlers should account for state changing while they await:

jsx
async function handleSubmit(event) {
  event.preventDefault();
  const submittedDraft = draft;
  setStatus("saving");

  try {
    await saveDraft(submittedDraft);
    setStatus("saved");
  } catch (error) {
    setStatus("failed");
  }
}

The local value intentionally records what was submitted.

Controlled inputs make draft ownership explicit ​

A controlled input receives its displayed value from React state and reports edits through an event. This is useful when rendering, validation, or submission depends on the draft:

jsx
function CatalogSearch({ search }) {
  const [query, setQuery] = useState("");
  const [status, setStatus] = useState({ kind: "idle" });

  async function handleSubmit(event) {
    event.preventDefault();
    const submittedQuery = query.trim();

    if (!submittedQuery) {
      setStatus({ kind: "invalid", message: "Enter a search term." });
      return;
    }

    setStatus({ kind: "loading" });
    try {
      const results = await search(submittedQuery);
      setStatus(results.length === 0
        ? { kind: "empty" }
        : { kind: "success", results });
    } catch (error) {
      setStatus({ kind: "failure", error });
    }
  }

  return (
    <form onSubmit={handleSubmit} role="search">
      <label htmlFor="catalog-query">Search the catalog</label>
      <input
        id="catalog-query"
        type="search"
        value={query}
        onChange={event => setQuery(event.currentTarget.value)}
        aria-describedby={status.kind === "invalid" ? "query-error" : undefined}
      />
      <button disabled={status.kind === "loading"}>Search</button>
      {status.kind === "invalid" && <p id="query-error">{status.message}</p>}
      {status.kind === "empty" && <p>No matching entries.</p>}
      {status.kind === "failure" && <p role="alert">Search failed.</p>}
      {status.kind === "success" && <ResultList items={status.results} />}
    </form>
  );
}

The draft belongs to the form because the form displays and submits it. The submitted value is captured before awaiting, so later typing does not change which request that handler initiated. A production search should also handle stale results or cancellation as shown below.

Not every input must be controlled. An uncontrolled input can be appropriate when the browser owns the draft and code reads it on submission, particularly for simple forms or file inputs. Choose based on who needs to observe and coordinate the intermediate value.

Effects synchronize with external systems ​

An effect is appropriate when rendering must be synchronized with something React does not own: a subscription, browser API, network connection, media element, or third-party widget.

jsx
useEffect(() => {
  document.title = `${transactions.length} transactions`;
}, [transactions.length]);

Dependencies are not an optimization list. They describe reactive values read by the effect. The linter can identify missing dependencies because a stale closure otherwise continues to see values from an older render.

Subscriptions return cleanup:

jsx
useEffect(() => {
  function handleOnline() {
    setOnline(true);
  }

  window.addEventListener("online", handleOnline);
  return () => window.removeEventListener("online", handleOnline);
}, []);

React runs cleanup before re-synchronizing and when the component unmounts. Development Strict Mode may mount, clean up, and mount again to expose effects that are not safely repeatable.

Do not use an effect for logic caused by a specific user action; keep that in the handler. Do not use one merely to derive state during rendering. Many effects disappear once ownership is modeled correctly.

Fetching in an effect needs stale-result handling:

jsx
useEffect(() => {
  const controller = new AbortController();

  async function load() {
    setState({ status: "loading" });
    try {
      const value = await loadTransactions({ signal: controller.signal });
      setState({ status: "success", value });
    } catch (error) {
      if (error.name !== "AbortError") {
        setState({ status: "failure", error });
      }
    }
  }

  void load();
  return () => controller.abort();
}, [accountId]);

Framework data loaders and query libraries often handle caching, deduplication, server rendering, and race policies better than hand-written effects. Effects remain important for understanding what those tools coordinate.

Refs retain values without causing renders ​

jsx
const inputRef = useRef(null);

function focusSearch() {
  inputRef.current?.focus();
}

return <input ref={inputRef} />;

Refs are appropriate for DOM nodes and mutable bookkeeping that does not affect visual output. If changing a value should update the UI, it belongs in state.

Hooks must be called at the top level of React components or custom hooks, not conditionally or inside loops. React associates hook state with call order. A custom hook packages stateful behavior and effects; it does not create shared state unless it connects to a shared external owner.

React becomes much easier to reason about when render stays a pure calculation, events represent user intent, state has a clear owner, and effects are reserved for synchronization beyond React.

Optional prompts ​

Predict: An asynchronous submit handler captures draft, awaits a save, and the user edits the form meanwhile. Which draft does that handler continue to use?

Answer: It sees the value captured by the render that created the handler. React state is a sequence of render snapshots, not a mutable variable that old closures automatically reread. Capturing const submittedDraft = draft makes that intentional and gives the in-flight operation a stable payload.

Explain: Why is an effect dependency array a description of the effect rather than a list of values chosen merely to reduce reruns?

Answer: Every reactive value read by the effect participates in what synchronization means. Omitting one can leave the effect connected using a stale closure. If reruns are excessive, change ownership, stabilize the relevant value, or separate effects by responsibility rather than lying about dependencies.

Debug: A component stores firstName, lastName, and fullName, then uses an effect to keep fullName synchronized. What should change?

Answer: Remove fullName state and compute it during render from the two source states. The effect creates an extra render and permits inconsistent snapshots. Derived data belongs in state only when it has independent user-controlled meaning rather than being a deterministic calculation.

Further reference ​