You add a search box to a todo list. todos is state, filter is state, and — because the filtered list "depends on" both — you add a third state variable, visibleTodos, and a useEffect that recalculates it whenever todos or filter changes. It works. Then, months later, someone adds a bulk "complete all" button that updates todos directly, and the list on screen doesn't change for a beat. No error, no warning — just a stale list until the next keystroke nudges the Effect awake.
Nothing here is exotic. It's one of the most common bugs in React codebases, and it exists because a value that should have been computed got stored instead.
This is episode two of React Deep Dive, on what React itself decides rather than JavaScript wearing a React import. This one is about a decision every component makes constantly and mostly gets right by accident: which values belong in state, and which ones only look like they do.
This article is written against React 19.3 (verified against the React blog and GitHub releases, 19.3.0, published September 9, 2026) and the React Compiler at 1.0, stable since October 2025. Everything here assumes React 19-era function components and hooks.
By the end of this article you'll be able to: Recognize derived state — a value fully determined by props or other state — versus state that genuinely needs to exist Explain, precisely, why syncing a derived value with useEffect + setState costs an extra render and can drift Replace that pattern with a plain calculation during render, and know when to reach for useMemo instead Handle the harder case — resetting or adjusting state when a prop changes — without an Effect Tell the difference between "derived from what I already have" and "genuinely new information," which is the actual boundary
You've written function components with useState and useEffect, and you've shipped at least one bug where two pieces of state disagreed with each other. No prior knowledge of the React Compiler is assumed.
Table of contents The problem: a list that lags behind its own data The mental model: state is memory, render is a formula Stage 1: the naive fix and why it still isn't right Stage 2: delete the state, keep the value Stage 3: when the calculation is actually expensive Stage 4: the harder case — resetting state when a prop changes Edge cases and gotchas Best practices: the actual test FAQ Cheat sheet Key takeaways
Here's the todo list from the opening, written the way it tends to get written the first time:
This runs. It even looks reasonable — visibleTodos "depends on" todos and filter, so it lives in an Effect that watches both. But walk through what actually happens on a single click of a filter button: setFilter("done") schedules a render. TodoList re-renders with the new filter but the old visibleTodos — React hasn't run your Effect yet, because Effects run after the DOM commits. The user briefly sees the wrong list (all todos, not just the done ones), for one paint. The Effect then runs, calls setVisibleTodos, and schedules a second render. React renders again, this time with the correct filtered list.
That's two full render passes and one commit doing visible work for a value you could have had correct on the first pass. And the bulk "complete all" bug from the intro is the same mechanism from the other direction: something mutates todos through a path that doesn't also re-run this specific Effect's mental model correctly, or a later render reads visibleTodos before the Effect catches up, and the two state variables disagree.
None of this is a React bug. React is doing exactly what you asked: keep two separate pieces of memory, and use an Effect to keep the second one following the first. The bug is that visibleTodos was never independent information — it was a formula wearing state's clothes.
Split every value your component touches into two categories: State is memory. It's the only thing React can't reconstruct on its own — user input, a value from a request, anything genuinely new that arrived from outside this render. A derived value is anything you could recompute, right now, from state and props you already have. It isn't information; it's a formula over information.
The mental model: if you can write const x = f(props, otherState) and get the right answer every time, x was never state — it's a calculation, and calculations belong in the render body, not in a useState/useEffect pair. An Effect exists to synchronize your component with something outside React — the DOM, a subscription, a network request, document.title. Using one to copy one piece of React state into another piece of React state is React talking to itself through a detour, and the detour is where the extra render and the drift both come from.
This reframes useEffect itself: it isn't "the place derived stuff goes," it's "the place synchronization with the outside world goes." A value computed from props and state was never outside anything.
A common first correction is to memoize inside the Effect, or to add a guard so it "only runs when needed":
Key concept: this treats the symptom (an extra render) without touching the cause (a second copy of information that has to be kept honest). The stale-paint flash from step 2 above is still there — the Effect still runs after the commit, not before it — and you've added a comparison that has to be maintained forever. The state was the mistake; no amount of guarding the Effect fixes that.
No visibleTodos state, no Effect, no second render, no drift — because there is only one piece of information (todos and filter) and one formula over it. Click a filter button now, and TodoList renders exactly once, with the correct list, because the correct list was never anything but todos and filter combined.
Key concept: a value that's recomputed on every render is not "wasted work" by default — rendering is supposed to be cheap and pure. Reach for a second render only when React genuinely needs one; a plain const inside the component body isn't a render, it's a step within the one you're already doing.
