Back to the Library

React setState Does Nothing - Same Value, No Re-render

5 min read
“The click registers. The function runs. setState gets called, with an argument, no typos, no errors. The number on screen does not move - because React looked at what you sent and decided nothing actually changed.”

In This Post

What React Checks Before Re-renderingThe Simplest Possible VersionThe Fix: Pass Something Actually DifferentThe Same Bug, One Layer DeeperThe Fix: A Value That's Guaranteed to DifferPractice These Patterns

Practice This Pattern

White Belt

Counter Never Updates

A counter whose increment button calls setCount(count) - passing the current value straight back. React sees no change and never re-renders.

+10 KI
Enter the Dojo
Black Belt

Force Refresh Does Nothing

A dashboard's Refresh button calls setRefreshKey(refreshKey) - the same value going back in. Both the re-render and the effect that depends on it are skipped.

+50 KI
Enter the Dojo
BugDojo
BlogFAQ

© 2026. Carved in code.

You click a button labeled "Increment." The click handler runs - put a console.log at the top and it fires every time. Inside, it calls setCount(...) with an argument that is, by every reasonable reading, correct.

The number on screen does not change. Not the first click, not the tenth.

There's no error. No warning. setState was called, with a value, and React just... didn't do anything with it.

What React Checks Before Re-rendering

useState's setter doesn't blindly trigger a re-render every time it's called. First, it compares the new value to the current value using Object.is - the same algorithm as ===, with a couple of edge cases for NaN and signed zero that don't matter here.

If Object.is(newValue, currentValue) is true - if the new value and the old value are, as far as JavaScript is concerned, the same value - React concludes nothing changed, and skips the re-render entirely. Not "renders and shows the same thing." Skips it. The component function doesn't run again, the JSX isn't recomputed, nothing.

This is an optimization, and most of the time it's invisible - usually when you call a setter, you're passing something genuinely different. The bug shows up when the value you compute and pass back happens to equal the value already there.

The Simplest Possible Version

export default function Counter() {
  const [count, setCount] = useState(0);
 
  function handleClick() {
    setCount(count);
  }
 
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
}

handleClick calls setCount(count). Read literally, that's "set count to... count." Whatever count currently is, pass that same thing back in.

count is 0. setCount(0). React compares Object.is(0, 0) - true. No change, no re-render. count is still 0, so the next click calls setCount(0) again, and the one after that, forever.

Symptom

The click handler runs every time - confirmed, no typos in the event wiring, no missing onClick. The displayed number simply never changes, on the first click or any subsequent one.

The Fix: Pass Something Actually Different

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

setCount(count + 1) passes 0 + 1 = 1 the first time. Object.is(1, 0) is false - genuinely different - so React proceeds with the re-render. count becomes 1, the component re-renders, Count: 1 appears, and the next click computes 1 + 1 = 2.

The fix isn't "make setCount work harder." It's "give it a value that isn't equal to what's already there" - which an increment naturally does, and a same-value passthrough naturally doesn't.

The Same Bug, One Layer Deeper

The first example is the bug in its most obvious form - setCount(count) practically announces itself. This one hides the exact same mechanism behind a "Refresh" button.

export default function RefreshPanel() {
  const [refreshKey, setRefreshKey] = useState(0);
  const [lastRefresh, setLastRefresh] = useState("Never");
 
  useEffect(() => {
    if (refreshKey === 0) return;
    setLastRefresh(new Date().toLocaleTimeString());
  }, [refreshKey]);
 
  function handleRefresh() {
    setRefreshKey(refreshKey);
  }
 
  return (
    <div>
      <p>Last refreshed: {lastRefresh}</p>
      <button onClick={handleRefresh}>Refresh</button>
    </div>
  );
}

The design intent is reasonable: clicking Refresh should change refreshKey, which the effect is watching, which should make the effect re-run and update the timestamp.

But handleRefresh calls setRefreshKey(refreshKey) - the current value, passed straight back. Object.is(refreshKey, refreshKey) is trivially true. React skips the re-render. Because there's no re-render, the effect that depends on refreshKey doesn't get a chance to re-evaluate either - both the render and the effect that was supposed to follow from it are skipped in one stroke. "Last refreshed: Never" stays exactly that, no matter how many times Refresh is clicked.

The Sensei's Hint

This version is harder to spot than the counter because refreshKey itself is never displayed - it exists purely to trigger something else. When a piece of state's only job is "change, so that something downstream notices," passing its current value back in defeats that entire job silently.

The Fix: A Value That's Guaranteed to Differ

function handleRefresh() {
  setRefreshKey((prev) => prev + 1);
}

(prev) => prev + 1 is a functional updater - it receives whatever refreshKey currently is and returns one more than that. 0 becomes 1, 1 becomes 2, and so on. Every call produces a value that is, by construction, different from the one before it. Object.is always returns false, the re-render always happens, and the effect watching refreshKey always gets to run.

Notice that refreshKey's actual numeric value is irrelevant to anything the user sees - it exists only as a "this changed, so re-check" signal. A functional updater that always increments is the standard way to build that kind of signal.

Constraints

Before calling a state setter, ask: "is there any path through this code where the new value could equal the current value?" If the new value is computed from the current value via something that can produce the same result - reading the same variable back, a calculation that can land on the same number - Object.is will catch it and silently skip the update. An increment, a toggle via functional updater, or a value from a genuinely different source (user input, a fetch result) doesn't have this problem.

Practice These Patterns

"Counter Never Updates" is the bug at its smallest - one line, setCount(count), nothing else going on. Get comfortable diagnosing it here, where the cause and the symptom are right next to each other. "Force Refresh Does Nothing" is the same line of code, setRefreshKey(refreshKey), but the consequence cascades into a useEffect that depends on the value - work through it to see how "skip the re-render" can quietly take "skip the effect that was supposed to run" down with it.