REACT: Under the Hood of UseState

Table of contents
- useState Isnโt Just a Variable โ Itโs a Slot in a List
- What Happens When You Call setCount(...)?
- What if multiple setCount calls happen before React re-renders?
- โ๏ธ During the next renderโฆ
- โ Why Devs Should Care
- ๐งฌ How React Processes Updates (Behind the Scenes Revisit)
- ๐ง Real-World Mental Model
- ๐งต Just Enough Fiber: How React Knows Which Hook Is Which
- ๐ Visual Recap: The useState Flow, Start to Finish
- ๐ง TL;DR โ How useState Really Works
- โญ๏ธBONUS: setCount(count + 1) vs setCount(c => c + 1)โ๏ธ
- ๐ In the Endโฆ
- ๐ง TL;DR
- ๐คฏ Final Thought

Did you know? React doesnโt store state in variables.
When you call:
const [count, setCount] = useState(0);
You might assume React stores count
in some internal object or variable tied to your component.
But thatโs not how it works.
โ React actually tracks state using:
A linked list of hook nodes
A circular queue of updates
And a strict render-time hook call order
Itโs not magic โ itโs just clever bookkeeping.
Letโs lift the hood and walk through how useState
works โ with real examples
useState
Isnโt Just a Variable โ Itโs a Slot in a List
const [count, setCount] = useState(0); //slot 0
const [text, useText] = useState(""); //slot 1
At first glance, this looks like a variable initialized with 0
.
But in React land, this means:
โHey React, on this render, I want the state stored at slot #0 in this componentโs hook list.โ
Yes โ slot 0. Not a name, not an ID โ just a position in a list. Like this:
So how does useState(0)
really work?
Hereโs what React does behind the scenes ๐
๐ข React assigns a slot based on the order of hook calls
๐ It stores the value 0
in a linked list of hooks(per component)
Component Fiber
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ hooks (linked list) โ
โ โ
โ Hook 0 โโถ Hook 1 โโถ null โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Each hook โ whether itโs useState
, useReducer
, or others โ becomes a node in a linked list attached to the componentโs Fiber node.
Why a list and not an array? Because:
Hook count can vary
Lists allow predictable traversal
And React re-creates this list fresh on every render, based on call order
๐ฑ Why You Canโt Call Hooks Conditionally
Ever seen this?
โ ๏ธ โRendered fewer hooks than expected.โ
Thatโs React telling you:
โHey, your hook list is broken. I canโt align state to the right slots anymore.โ
Example:
if (isLoggedIn) {
useState("hi"); // โ DON'T
}
This messes up the hook slots. On next render, useState("hi")
might become slot 0 instead of slot 1.
Now every hook is getting the wrong state.
๐ฅ TL;DR: Hook call order = sacred. Never call hooks conditionally.
๐ง Real-World Takeaway
This explains bugs like:
Stale state
Hooks acting "out of sync"
setState
updating the wrong value (or nothing at all)
React doesnโt remember variable names โ it relies entirely on hook call order during render.
What Happens When You Call setCount(...)
?
So far, weโve seen how useState
assigns a slot in a linked list to hold your state.
But what happens when you actually update that state?
setCount(c => c + 1);
You might expect the state to update immediately โ but React doesnโt do that. Instead, it pushes the update into a queue, and schedules a re-render.
๐ฆ When setCount(newValue)
is called:
๐ React schedules a re-render
๐ง During render, it goes back to the same slot (slot 0)
๐ Retrieves the new updated state
๐งฉ Then it compares ๐ the old and new virtual DOM
๐จ If thereโs a change, React updates the UI
This flow is what powers Reactโs magic โ hooks + virtual DOM + Fiber working together.
๐ React uses a circular linked list of updates
Each hook node (like Hook 0
for count
) has an internal queue
that holds pending updates.
Each hook is a node in a linked list ๐
Hereโs what each node holds:
๐ง memoizedState โ the actual state value
๐ฅ queue โ list of pending setState calls
โญ๏ธ next โ pointer to the next hook in this component
This structure is rebuilt on every render โ in the same order!
Letโs visualize it.
Hook 0
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ memoizedState: โ 0
โ queue.pending: โ null
โ next: โ โ Hook 1
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Hook 1
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ memoizedState: โ ""
โ queue.pending: โ null
โ next: โ โ null
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Now you call:
setCount(c => c + 1);
๐ฌ React pushes an update into the queue
Hook 0
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ memoizedState โ 0
โ queue.pending โ โโโโโโโโโโโโโโโ
โ next โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโ
โ action: f (c + 1) โ
โ next โโโโโโโโโโโโโโโ (points to self)
โโโโโโโโโโโโโโโโโโโโโโ
Hook 1 (unchanged)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ memoizedState โ ""
โ queue.pending โ null
โ next โ null
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ React uses a circular list here โ Because thereโs only one update,
next
points to itself forming a circular list with one item. โ soupdate.next
points to itself if it's the only item.
Why circular? Because it's fast to:
Add new updates
Walk the queue in order
Wrap around without null-checks
โ What Happens on Re-render?
During the next render cycle:
React reaches
Hook 0
againSees there's a
queue.pending
Walks the circular list and applies all updates in order
Updates
memoizedState
Clears the queue
Example:
// Initial state
memoizedState = 0
// apply setCount(c => c + 1)
memoizedState = 1
// clear queue
queue.pending = null
Boom โ count
becomes 1 in the next render. ๐ง
๐ก Real-World Takeaway
State updates are async because React batches them in queues
setState doesnโt update immediately โ it just pushes to the queue
You can have multiple queued updates before the next render
This explains:
Why
console.log(count)
right aftersetCount(...)
shows stale dataWhy batching works (like in event handlers or
startTransition
)Why
setState
inside loops can behave unexpectedly
What if multiple setCount
calls happen before React re-renders?
Letโs say you run this in an event handler:
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
These donโt immediately re-render. Instead, React queues them up in a circular linked list inside Hook 0
:
Hook 0 (count)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ memoizedState: 0 โ
โ queue.pending โ โโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โฒ โโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโถ update 1 โ
โ action: f โ
โ next โโโโโโโโผโถ update 2
โโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโ
โ update 2 โ
โ action: f โ
โ next โโโโโโโโผโถ update 3
โโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโ
โ update 3 โ
โ action: f โ
โ next โโโโโโโโ โ points to update 1
โโโโโโโโโโโโโโโ (last update links back to first)
โฒ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ Updates are queued as a circular list:
eachsetCount
adds a node, and React later walks through all of them in order.
โ๏ธ During the next renderโฆ
React performs this:
Starts from
memoizedState = 0
Applies update 1:
c => c + 1
โ1
Applies update 2:
c => c + 1
โ2
Applies update 3:
c => c + 1
โ3
Sets
memoizedState = 3
Clears the queue.
๐ฏ Final
count
value will be 3, not 1 โ because all updates used the latest state (c => c + 1
).
โ Why Devs Should Care
setCount(x + 1)
(non-functional) would overwrite and only apply once.setCount(c => c + 1)
stacks โ each update gets applied.You donโt need
useEffect()
tricks for batching; Reactโs queue handles this.React batches updates and avoids redundant renders efficiently using this structure.
If you queue multiple updates using functional form (
c => c + 1
), React handles them correctly.If you use non-functional form (
setCount(count + 1)
), they may overwrite each other. โ ๏ธ
๐งฌ How React Processes Updates (Behind the Scenes Revisit)
When a re-render is triggered (due to setState
), React walks the list of hooks in the exact same order as before.
Each hook looks like this internally:
HookNode {
memoizedState: current state value,
queue: {
pending: circular linked list of updates
},
next: โ next HookNode
}
Letโs revisit this example:
const [count, setCount] = useState(0); // slot 0
const [text, setText] = useState(""); // slot 1
React builds a linked list of hooks for this component:
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
โ Hook 0 โ โโโถ โ Hook 1 โ โโโถ null
โ count = 0 โ โ text = "" โ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
Letโs say setCount(c => c + 1)
was called 3 times.
During the next render:
React walks hook list โ starts with Hook 0
Sees updates in queue:
Applies all 3
c => c + 1
updatesFinal value becomes
3
Moves to Hook 1 โ No updates โ Skips.
Hook 0 (updated)
โโโโโโโโโโโโโโโโโโโโโโ
โ memoizedState: 3 โ
โ queue.pending: nullโ
โโโโโโโโโโโโโโโโโโโโโโ
Hook 1 (unchanged)
โโโโโโโโโโโโโโโโโโโโโโ
โ memoizedState: "" โ
โ queue: null โ
โโโโโโโโโโโโโโโโโโโโโโ
๐ง Real-World Mental Model
React doesnโt "remember" state via variable names.
It tracks state slot-by-slot via hook order. Hook 0 will always be count
.
Changing the order of hooks between renders?
๐ฅ Boom โ React throws:
"Rendered fewer hooks than expected..."
๐งต Just Enough Fiber: How React Knows Which Hook Is Which
React's secret sauce? A data structure called Fiber.
Each component gets its own Fiber Node โ a JavaScript object that holds everything React needs to track during rendering.
Hereโs what a simplified Fiber looks like:
FiberNode {
memoizedState โ points to head of hook list
return โ parent fiber
child โ first child
sibling โ next sibling
...
}
๐ฏ memoizedState
is the key.
React stores the head of the hook linked list on the component's Fiber.
As your component renders, React walks that list one hook at a time โ in order.
๐ฆ During First Render:
For our example:
const [count, setCount] = useState(0); // slot 0
const [text, setText] = useState(""); // slot 1
React does:
Fiber.memoizedState โโโถ Hook 0 โโโถ Hook 1 โโโถ null
count=0 text=""
Each useState()
call creates a Hook node and links it to the previous one.
๐ During Re-render:
Same order. No surprises.
React sets a pointer to
Fiber.memoizedState
Every time a hook is called, React moves to the
next
hook nodeIt retrieves
memoizedState
, applies any updates, and moves on
Fiber.memoizedState โโถ Hook 0 โโถ Hook 1
(current hook pointer walks one by one)
๐ก Thatโs why hook order must be consistent across renders. If it changes, React can't match the state correctly.
โ๏ธ Real-World Framing
Want to build custom hooks? Now you know they're just functions called in order.
Using
React DevTools
and seeingHooks (count, text)
? Thatโs this list.Got a bug with mismatched state? Might be breaking hook order internally.
๐ Visual Recap: The useState
Flow, Start to Finish
Let's put the whole story together with ASCII diagrams:
๐งฑ Initial Render:
const [count, setCount] = useState(0); // slot 0
const [text, setText] = useState(""); // slot 1
React:
Fiber.memoizedState
โ
โผ
โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ
โ Hook 0 โโโโโถโ Hook 1 โโโโถ null
โ state: 0 โ โ state: "" โ
โ queue: null โ โ queue: null โ
โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ
โฉ After setCount(c => c + 1):
Hook 0
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ memoizedState โ 0
โ queue.pending โ โโโโโโโโโโโโโโโ
โ next โโโโโโโโโโโโโโโโโโโโโโโโถโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโ
โ update.action: f โ
โ update.next โโโโโโโโ (points to self)
โโโโโโโโโโโโโโโโโโโโโโ
Hook 1 (unchanged)
โโโโโโโโโโโโโโโโโโโโโโ
โ memoizedState: "" โ
โ queue: null โ
โ next: null โ
โโโโโโโโโโโโโโโโโโโโโโ
๐ During Re-render:
React walks the hook list via
next
Applies each hookโs queue
Updates
memoizedState
Resets queue to null
Hook 0
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ memoizedState โ 1 โ
โ queue.pending โ null
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Hook 1 stays unchanged.
๐ง TL;DR โ How useState
Really Works
Hooks form a linked list stored on the componentโs Fiber
useState()
allocates a slot based on call ordersetState()
pushes updates into a circular queueOn re-render, React:
Walks the hook list
Applies all pending updates
Updates
memoizedState
Fiberโs
memoizedState
โ head of the hook list๐ฅ Hook order must never change
โญ๏ธBONUS: setCount(count + 1)
vs setCount(c => c + 1)
โ๏ธ
Letโs say count = 0
, and you write:
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
You might expect count
to become 3
, but insteadโฆ it becomes 1
.
โ Why?
count + 1
is evaluated immediately, not lazily.
So each line becomes:
jsCopyEditsetCount(1); // because count is still 0
setCount(1);
setCount(1);
Only the last one wins.
โ Instead, use the functional form:
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
Each call receives the latest state, even if queued.
So:
0 โ 1 โ 2 โ 3 โ
๐งช Real-World Example
In a counter button:
jsCopyEditonClick={() => {
setCount(count + 1); // โ always sets to 1 if clicked fast
}}
onClick={() => {
setCount(c => c + 1); // โ
properly increments
}}
๐ก Takeaway
Use
setCount(prev => prev + 1)
when the next state depends on the previous.Especially important when:
Scheduling multiple updates
React batches them
Working with async effects
๐ In the Endโฆ
Why Should You Care?
This wasnโt just a nerdy dive into React internals โ it has real-world impact:
โ
Fix bugs you didnโt know existed
Understanding that setState
is async and batched helps you avoid subtle bugs in counters, toggles, and complex UI flows.
๐ Predict re-renders confidently
Knowing how hooks are matched by order and updates are queued means you can refactor without fear.
๐ฏ Write faster, safer components
Use functional updates where needed. Avoid unnecessary state. Debug smarter.
๐ง TL;DR
React doesnโt โstoreโ state in variables.
It:
๐ Uses a linked list of hooks per component.
๐ Assigns each
useState
a slot based on order.๐ Queues updates via a circular linked list.
๐งฎ Applies updates during re-render using the Fiber architecture.
const [count, setCount] = useState(0); // slot 0
const [text, setText] = useState(""); // slot 1
Behind the scenes:
count = hook 0 โ memoized state = 0
setCount(x) โ pushes an update to hook 0's queue
During next render: React runs reducer logic, gets the new state
Updates memoized state, clears queue
๐คฏ Final Thought
Reactโs useState
isnโt just a simple getter/setter โ itโs a finely tuned dance of linked lists, update queues, and render orchestration.
Next time you write setCount(c => c + 1)
โ know that somewhere deep in Fiber, a tiny linked list is hard at work.
Subscribe to my newsletter
Read articles from Omm Pani directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
