Blog #28: The naivety of believing setState is an immediate assignment
I once thought calling setCounter(count + 1) meant the line of code right after would get the new value. This mistake cost me dozens of hours debugging 'ghostly' logic.
I admit I once had a very naive view of useState. In my mind back then, setCount(5) was no different from an assignment count = 5. I believed that immediately after calling that function, the data in the machine would be updated.
I believed it because in traditional programming languages like C++ or Java, once you assign a value, that variable changes immediately. I brought that mindset to React and encountered a serious mishap in a "multi-step order confirmation" feature.
1. Context: When data "froze"
I was building a multi-step form. When the user clicks "Next," I update currentStep and then call another function to fetch data based on that new step.
const handleNext = () => {
setStep(step + 1);
console.log(step); // Why does it still show the old value?
fetchStepData(step); // Sends API for the wrong step, causing serious data errors for the user
};
The result: The whole system was out of sync. I frantically added setTimeout to "wait" for the state to change, or worse, used useEffect just to catch that change. The entire code became a trash heap of patches.
2. Concept: State is a Snapshot
The problem isn't that React is slow, but that State in a single render is immutable. When you call setStep, you aren't changing the value of the existing step variable. You are asking React: "Hey, use the new step on the next render". But the current code is still holding the old "Snapshot" of step.
3. Practical Approach and Balance
A Junior will encounter this at least once a week. They often resolve it by using useEffect to observe the state, making the logic flow fragmented and extremely difficult to follow.
Comparison of two ways:
- The "Naive" way: Call
setStateand then use that state right in the next line. Leads to logic bugs. - The Practical way (Functional Updates & Variables):
- If you need the new value immediately to do something else, calculate it as a local variable (
const nextStep = step + 1). - Or use Functional Update:
setCount(prev => prev + 1).
- If you need the new value immediately to do something else, calculate it as a local variable (
// Sustainable way
const handleNext = () => {
const nextStep = step + 1; // Calculate the new value
setStep(nextStep); // Request React to update later
fetchStepData(nextStep); // Use the newly calculated value immediately for other tasks
};
Lesson Learned
I learned that: Never trust the value of a State immediately after you've called its update function.
- When the old way (careful) is still correct: Almost never. You must always understand that
setStateis asynchronous from the perspective of the currently executing code. - When to avoid: Avoid using State as the "sole source of truth" for logic running immediately after it. Use local variables or function parameters.
Clarity about React's execution flow is the boundary between a coder and an engineer.
Notes on the day I stopped waiting for immediacy from React's promises.
Series • Part 28 of 50