Skip to content

Build1 publisher3 min readPublished

Syncing derived state through useEffect costs a second render pass per click

React runs Effects after the DOM commits, so a component that keeps its filtered list in state renders once with the stale list and again with the correct one. A dev.to walkthrough traces both passes and the fault that outlives them.

The Engineer · Build desk

Illustration accompanying Syncing derived state through useEffect costs a second render pass per click

What happened

  • A dev.to walkthrough of React derived state keeps a filtered todo list in a third useState and syncs it with a useEffect whose dependency array names both todos and filter.
  • Because React runs Effects after the DOM commits, the click that sets the filter re-renders the component with the new filter and the old stored list.
  • The Effect then calls setVisibleTodos and schedules a second render, so a single filter click runs two full render passes and one commit for a value the first pass could have had right.
  • The post opens on the durable version of the fault: a bulk complete-all button added months later writes to todos directly, and the on-screen list stays stale until the next keystroke.
  • The article is written against React 19.3, published September 9, 2026, and the React Compiler at 1.0, stable since October 2025.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint A stored derived value is only as fresh as the last code path that remembered to update it, so every future writer to the source array inherits an obligation that nothing in the file states.
  • cost The extra render pass is paid per interaction, so the bill scales with how much the user clicks rather than with the size of the component.
  • exposure Detection falls to a human noticing a list that lags, because the divergence throws nothing and logs nothing.
  • decision Auditing a component now has one concrete question to ask of each useState: could React rebuild this value from what it already holds?

The file reads correctly to a reviewer. The dependency array names both inputs, the Effect recomputes when either changes, and the component works the day it ships [1]. The dev.to post's account of why the fault lasts is timing: the second writer to `todos` arrives months later, as a bulk complete-all button that updates the array directly [6]. The post names the failure as two state variables disagreeing, either because a path mutates `todos` without re-running the Effect's assumptions or because a later render reads `visibleTodos` before the Effect catches up [21]. "None of this is a React bug," the post says, since React is keeping two pieces of memory and using an Effect to make one follow the other, exactly as written [20].

The post makes a maintenance argument, and it does not mention code review at all. The component's correctness depends on every future writer of `todos` knowing that a second copy exists.

Two render passes per click is a measurement of this component [5]. Whether it costs a user anything depends on whether the intermediate value looks different from the final one. In the walkthrough it does: the first pass paints every todo when the user asked for the done ones [4]. When the stale value happens to match the new one, the second pass is overhead nobody sees [16].

The extra pass comes from a `setState` inside an Effect [5]. Caching the calculation is a different cost, so the React Compiler at 1.0 does not address it [17].

The post's replacement is a plain calculation during render, with `useMemo` held back for the case where the calculation is actually expensive [11][12]. Deleting the state does not delete the work. The same `todos.filter()` call moves into the render body, and what goes away is the scheduled second render and the second copy of the list [18].

One case gets its own stage: resetting or adjusting state when a prop changes, without an Effect [13]. Here the value really is memory, and it still has to change when something outside it changes. Computing during render does not cover that.

The boundary the post offers is reconstructability. State is the only thing React cannot rebuild on its own, meaning user input, a value from a request, anything genuinely new that arrived from outside this render [14]. Everything else is a formula you decided to store. The post calls that mistake one of the most common bugs in React codebases and wrote that it "exists because a value that should have been computed got stored instead" [15][10]. The prevalence figure is one author's assessment. The post gives no measurement for it.

What to watch

  • A dev-mode warning that flags a setState inside an Effect whose deps are the values it derives from would move this from a maintenance problem to a lint problem.
  • Whether later episodes put a threshold on stage 3, which currently defines the useMemo case only as a calculation that is actually expensive.
  • Whether React Compiler releases after 1.0 touch Effect-scheduled renders at all, or stay on memoising computed output.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories