Volume I index
State and Events From Scratch

State Updates and Renders

Ishtmeet Singh @ishtms/July 14, 2026/9 min read
#react#state#rendering#batching#updater-functions

Calling a state setter requests another render. It does not change the state variable inside the handler that is already running.

jsx
function handleClick() {
  setCount(count + 1);
  console.log(count);
}

If the button displayed zero before the click, the log prints zero. That render created both the count binding and the handler.

The next render creates another count binding with the next state. React does not rewrite the local variable in the completed render.

Every Render Receives a State Snapshot

Consider the first two renders of Counter.

text
render 1 receives count 0
render 1 creates a handler that sees count 0
click calls that handler
handler requests count 1
render 2 receives count 1
render 2 creates a handler that sees count 1

Each handler closes over the variables from its own component call. A handler from render one continues to see render one's values even after React has produced render two.

This becomes visible after a delay.

jsx
function handleClick() {
  setCount(count + 1);

  setTimeout(() => {
    console.log(count);
  }, 1000);
}

The timeout callback closes over the same count as the click handler. It prints that snapshot one second later.

Do not use a timeout merely to wait for state to change. Put UI calculation in the next render, and put follow-on user work in the same event when it already has the needed next value.

React Processes Updates After the Handler

React batches state updates during an interaction. It waits until the event handler has finished before processing the queued work and committing the next screen.

jsx
function handleSave() {
  setSaved(true);
  setMessage("Goal saved");
}

React can process both requests together and render one consistent next result. The user does not need an intermediate screen with saved changed but an old message.

Batching also means the DOM is not updated between those two setter calls.

jsx
function handleSave() {
  setSaved(true);
  console.log(statusElement.textContent);
}

The DOM read can still show the previous committed text. React has not committed the requested state yet.

Most application code should not read React-managed DOM immediately after a setter. Calculate UI from state and let the next commit update the document.

Repeated Value Updates Use One Snapshot

This handler calls the setter three times.

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

All three expressions read the same count from the current render. If it is zero, each call becomes setCount(1).

text
request replace with 1
request replace with 1
request replace with 1

The next state is one, not three.

The calls did not execute a live increment operation against mutable React state. They each calculated a replacement value from one snapshot.

Updater Functions Use the Queued Value

Pass a function when the next state depends on the state already in the update queue.

jsx
function handleAddThree() {
  setCount((current) => current + 1);
  setCount((current) => current + 1);
  setCount((current) => current + 1);
}

React queues the functions. During update processing, the result from one becomes the input to the next.

text
start with 0
first updater receives 0 and returns 1
second updater receives 1 and returns 2
third updater receives 2 and returns 3

The next render receives three.

The parameter name is local to each updater.

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

current, previous, and value are ordinary names. Choose one that identifies the state being updated.

A direct value remains clear when the next state does not depend on the previous state.

jsx
setStatus("complete");
setQuery("");

Replacement Values and Updaters Share One Queue

React processes updates in call order.

jsx
setCount(5);
setCount((current) => current + 1);

The replacement establishes five for the queued result. The updater receives five and returns six.

Reverse the order.

jsx
setCount((current) => current + 1);
setCount(5);

The final replacement requests five, so the next state is five.

One handler rarely needs a long mix of replacements and updaters. When it does, keep the queue short enough to read in call order and add a test for the resulting interaction later.

Updater Functions Must Be Pure

An updater receives pending state and returns next state. It must not change the pending value or perform unrelated work.

jsx
setLesson((current) => {
  current.complete = true;
  return current;
});

This mutates the state object and returns the same reference.

Return another object.

jsx
setLesson((current) => ({
  ...current,
  complete: true,
}));

Development Strict Mode can call updater functions twice and ignore one result to expose impurities. A pure updater can run again with the same input without changing external data.

Do not log analytics, write storage, or send a request inside an updater. Put user-event work in the event handler and let the updater calculate only the next state value.

Replace State Objects Instead of Mutating Them

State can hold an object.

jsx
const [lesson, setLesson] = useState({
  title: "State Updates",
  complete: false,
});

This mutation changes the existing object.

jsx
lesson.complete = true;
setLesson(lesson);

The state reference passed to the setter is the same reference React already holds. React compares state values with Object.is, so it can treat the request as unchanged. The previous state was also modified, removing the old snapshot needed for predictable rendering and debugging.

Create another object.

jsx
setLesson((current) => ({
  ...current,
  complete: true,
}));

Object spread copies the top level and the later property supplies the changed value. The copy is shallow, so nested changes require copying each changed level.

jsx
setLesson((current) => ({
  ...current,
  author: {
    ...current.author,
    name: "Ishtmeet",
  },
}));

Replace State Arrays with New Arrays

Add one lesson without changing the existing array.

jsx
setLessons((current) => [
  ...current,
  newLesson,
]);

Remove one through filter.

jsx
setLessons((current) =>
  current.filter((lesson) => lesson.id !== lessonId),
);

Update one record through map.

jsx
setLessons((current) =>
  current.map((lesson) =>
    lesson.id === lessonId
      ? { ...lesson, complete: true }
      : lesson,
  ),
);

Unchanged records keep their existing object references. The selected record receives a new object. The array itself is new.

Avoid push, pop, splice, sort, and reverse on the current state array. Use non-mutating operations such as map, filter, toSorted, and spread.

Same-Value State Can Be Skipped

React uses Object.is to compare the next state with the current state.

jsx
setCount(0);

If the state is already zero, React can skip work for that update. No new visible value exists to commit.

For objects and arrays, Object.is compares references.

jsx
Object.is({}, {}); // false
Object.is(lesson, lesson); // true

Creating a new object for a real state change provides a new reference. Creating a new object with identical fields on every event still requests another value and can produce unnecessary rendering.

Do not add deep equality checks inside every setter. Create a new state value when the stored information changes.

React can still call a component before deciding that a same-value update can be discarded in selected cases. Application correctness must depend on pure rendering, not on a promise that the function is never called.

Trace Render and Commit After State

A state update supplies a concrete trigger for another render.

text
button click
  -> handler reads its snapshot
  -> setter queues next state
  -> handler finishes
  -> React renders affected components
  -> React compares next output with current output
  -> React DOM commits required host changes
  -> browser paints

The component function runs during render. A changed state value does not imply every returned DOM node changes during commit.

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

  return (
    <section>
      <h2>Lesson counter</h2>
      <button onClick={() => setCount((n) => n + 1)}>
        {count}
      </button>
    </section>
  );
}

The heading output remains the same. React DOM can retain its host text while updating the button text.

Strict Mode Exposes Render-Time Mutations

The Vite entry wraps the app in StrictMode during development.

jsx
<StrictMode>
  <App />
</StrictMode>

React can call component functions, state initializers, and updater functions again during development checks. One result is discarded.

This component mutation produces a different array length on repeated calls.

jsx
function LessonList({ lessons }) {
  lessons.push({ id: "extra", title: "Extra" });
  return <p>{lessons.length}</p>;
}

Remove the mutation and calculate from current inputs.

Strict Mode does not call the user event handler twice. The button click occurs once. The updater queued by that handler can be called again to verify its purity.

State Setters Keep a Stable Identity

React returns the same setter function for a state position across renders.

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

The local count binding changes with state snapshots. The setCount function identity remains stable.

This lets a component pass the setter through a callback prop when that public contract is appropriate, though named action callbacks often communicate intent better.

jsx
<CounterControls onIncrement={() => setCount((n) => n + 1)} />

State setters have stable identity. No Effect is needed for this counter.

Debug One Update

Add logs around the setter.

jsx
function handleClick() {
  console.log("handler snapshot", count);
  setCount((current) => current + 1);
}

console.log("render snapshot", count);

Click once in development. Strict Mode can produce more render logs than commits, but one click produces one handler log. The next committed screen displays the updater result.

React DevTools can show the current state after the render. The browser Elements panel can show the committed text. Use the logs only to connect handler timing, then remove them.

Lesson Check

The update model now supports exact predictions.

text
state is fixed within one render
setters queue values or updater functions
React batches compatible requests
updaters process in order from queued state
objects and arrays receive new references for changes
render calculates the next tree
commit applies only required host work

Source Notes