Back to the Library

React Derived State Is Always One Step Behind - Stop Using useEffect

5 min read
“You synced one piece of state to another with useEffect, and it works - except it's always exactly one render late. The fix isn't a better effect. It's no effect at all.”

In This Post

The Setup That Looks ReasonableThe "Fix" That Creates a New BugThe Actual Fix: Don't Store ItWhy This Feels Counterintuitive at FirstWhen useMemo Belongs Here InsteadPractice These Patterns

Practice This Pattern

Blue Belt

Doubled Counter Frozen at Zero Never Updates

A counter that displays its value alongside a doubled version - Count updates immediately on click, but Doubled is calculated in a useEffect with an empty dependency array and never recalculates.

+25 KI
Enter the Dojo
BugDojo
BlogFAQ

© 2026. Carved in code.

You have two numbers on screen. One is count. The other is count * 2, labeled "Doubled."

Click the increment button. Count updates immediately, like it should. Doubled does not move. It stays at whatever it was when the page loaded - in this case, 0 - forever.

The Setup That Looks Reasonable

export default function DoubledCounter() {
  const [count, setCount] = useState(0);
  const [doubled, setDoubled] = useState(0);
 
  useEffect(() => {
    setDoubled(count * 2);
  }, []);
 
  return (
    <div>
      <p>Count: {count}</p>
      <p>Doubled: {doubled}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
    </div>
  );
}

This reads like a sensible plan: "whenever count changes, recompute doubled to match." A useEffect that watches count and calls setDoubled sounds like exactly the tool for "whenever X changes, do Y."

Except the dependency array here is [] - so the effect runs once, on mount, computes 0 * 2 = 0, sets doubled to 0, and never runs again. Click Increment ten times: count becomes 10, the effect that's supposed to keep doubled in sync never re-fires, and doubled is still 0.

Symptom

Count responds to every click instantly. Doubled does not respond at all - it's frozen at its initial value no matter how many times you click.

The "Fix" That Creates a New Bug

The obvious next step is to add count to the dependency array:

useEffect(() => {
  setDoubled(count * 2);
}, [count]);

This actually works, in the sense that doubled now updates. But it introduces a one-render lag that's easy to miss in a quick test and very visible under fast interaction.

Click Increment. count becomes 1. React re-renders - Count: 1, Doubled: 0 (the old value, because the effect hasn't run yet for this render). After that render commits, React runs the effect, which calls setDoubled(2), which schedules another render - Count: 1, Doubled: 2, finally.

Every click produces two renders instead of one, and for one frame, the two numbers on screen are out of sync with each other - Doubled is always showing the relationship for the previous value of count. Click rapidly enough, or test with multiple increments in a row, and Doubled visibly lags one step behind Count.

The Sensei's Hint

This is a real, working fix for the original bug - "Doubled" does update now. But "it works, with a one-render lag" is itself the signature of derived state implemented with useEffect. If a value can be computed directly from other state, an effect-based sync will always be at least one render behind that computation.

The Actual Fix: Don't Store It

export default function DoubledCounter() {
  const [count, setCount] = useState(0);
  const doubled = count * 2;
 
  return (
    <div>
      <p>Count: {count}</p>
      <p>Doubled: {doubled}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
    </div>
  );
}

No useState for doubled. No useEffect. Just a const that runs every time the component renders, computing count * 2 from whatever count currently is, right now, in this render.

Click Increment. count becomes 1. The component re-renders. The line const doubled = count * 2 runs again - this time with count equal to 1 - and produces 2. Both numbers update in the exact same render. There is no second render, no lag, no window where they disagree.

Why This Feels Counterintuitive at First

useEffect is the tool for "when X changes, do Y" - and "when count changes, update doubled" sounds exactly like that. The distinction that matters is what kind of thing Y is.

If "do Y" means causing something outside React - a network request, a subscription, writing to localStorage, focusing a DOM node - that's a side effect, and useEffect is correct.

If "do Y" means computing a value that React itself will render - and that value is a pure function of state or props you already have - it isn't a side effect at all. It's just a calculation, and calculations belong in the render body, recomputed fresh every time, the same way count + 1 or items.length would be.

Constraints

Ask: "could I write this as const x = someExpression(otherState), directly in the component body, with no hook?" If yes - and the expression isn't expensive enough to need useMemo - that's the entire implementation. Reaching for useState + useEffect to keep a derived value "in sync" produces a value that is, structurally, always one render behind the thing it's derived from.

When useMemo Belongs Here Instead

If the calculation is genuinely expensive - filtering a huge list, running a heavy computation - a plain const would still be correct, but it would re-run on every render of the component, including renders triggered by unrelated state changes. useMemo lets you cache the result and only recompute when its specific dependencies change:

const doubled = useMemo(() => count * 2, [count]);

For count * 2, this is overkill - the calculation is cheaper than the useMemo bookkeeping itself. But the shape is the same as the plain const version: synchronous, same-render, no lag. The choice between a plain const and useMemo is a performance question. The choice between either of those and useState + useEffect is a correctness question.

Practice These Patterns

"Doubled Counter Frozen at Zero Never Updates" is the single kata for this pattern, and it's worth working through twice: once to get doubled updating at all (the useEffect with [count] version, watching for the one-render lag), and a second time to delete the effect entirely and replace it with a plain const. The second pass is the one that should feel like the real fix.