This article is published in English.
Where useState Keeps Its Value: React Elements Versus Fibers
Why a React function component forgets everything between calls, why elements cannot hold state, and how the fiber's memoizedState field keeps useState values alive.
A function component is just a function, and functions forget their local variables the moment they return. Yet useState hands back the updated value on every render, as if the function remembered. This piece answers one narrow question precisely: where does that value physically live between renders? By the end you will be able to tell apart the two objects React builds for every component, the element and the fiber, and explain which one carries state and why.
The puzzle: a function with no memory
Start with the most familiar component there is, a counter:
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Click once and it shows 1, click again and it shows 2. Nothing surprising, until you look at it as plain JavaScript. Counter is a function. Each time a function is invoked, its local variables are created from nothing, and when it returns they are gone. A second invocation starts over entirely.
By that logic, the second time React calls Counter(), the line useState(0) ought to produce 0 again. It produces 1. Something outside the function must be holding the number from one call to the next. The function body cannot be it, because that is simply not how functions behave. So what is?
Ruling out the element
There is an obvious candidate that turns out to be wrong, and eliminating it makes the real answer clearer.
The line <button onClick={...}>{count}</button> is JSX. Browsers never run JSX; a compiler such as Babel or the TypeScript compiler rewrites it into a plain function call before the code ships. Conceptually, the result is:
React.createElement("button", { onClick: fn }, count)
With the automatic JSX runtime introduced in React 17, the compiled call is actually jsx() from react/jsx-runtime rather than React.createElement, but both do the same job. Whatever the function is called, it returns an ordinary object:
{
type: "button",
props: { onClick: fn, children: 1 },
}
React calls this an element. It is a description, not a live thing: a small record saying that a button should appear here, with this click handler, displaying this number. Real elements carry a few more fields, such as key, ref and an internal $typeof marker, but none of them store history.
Now the crucial observation. Every call to Counter builds a brand-new element. Click the button, Counter() runs, a fresh object is returned, and the previous one becomes garbage. If state lived on the element, there would be no way to connect render two to render one, because the render-one object no longer exists when render two begins.
That disposability is deliberate. Elements are cheap precisely because they are rebuilt from scratch every render and never need to be kept in sync with anything. It also means they cannot be where count is stored.
The object that survives: the fiber
Next to the elements, React keeps another, longer-lived object for each mounted component. It is not rebuilt on each render. It is created when the component first mounts and then kept and updated for as long as the component stays on screen. This is the fiber.
Fibers are often described as something mysterious, but at the lowest level a fiber is a plain JavaScript object. There is no special runtime construct behind it. If you pause in a debugger inside React's reconciler and inspect one, you will find an ordinary object with a set of properties, the kind you could write yourself with a pair of curly braces. You can also peek at them from the browser: React attaches internal keys to DOM nodes that point at their fibers, which is how React DevTools finds them.
Rather than listing every field at once, it helps to build the fiber for Counter one property at a time. Right after mount, a minimal version looks like this:
{
type: Counter,
}
type: whose bookkeeping this is
type is the simplest field. It identifies which component this fiber tracks. type: Counter means the object exists to manage an instance of Counter, holding a reference to the function itself.
Host elements get fibers too. The button that Counter renders has its own fiber, and its type is the string "button" rather than a function. So a <Counter /> fiber has { type: Counter } and the <button> fiber has { type: "button" }: same field, same purpose, pointing either at your component or at a built-in tag.
type also plays a role in reconciliation. When React compares a new element with an existing fiber at the same position, a matching type lets it reuse the fiber and its state, while a different type makes it throw the old fiber away and create a new one. That is why switching between two different components in the same spot resets their state.
On its own, though, type says nothing about how state persists. For that you need the next field.
memoizedState: where the count actually lives
useState needs a place outside the function to keep the current value, since the function's locals are reset on every call. That place is a field on the fiber named memoizedState. "Memoized" simply means remembered: held over from before instead of recomputed.
Adding it to the sketch gives:
{
type: Counter,
memoizedState: { count: 0 },
}
This is a simplified picture. In the real implementation, memoizedState on a function component's fiber points to the first of a linked list of hook objects, one per hook call, and the number 0 sits in that hook's own memoizedState field rather than in a { count: 0 } object. React has no idea your variable is called count; it only knows "the first hook's value". For understanding where state lives, the simplified version is enough.
Now follow a click. React calls Counter() again. When execution reaches useState(0), the hook does not return the 0 in your code. It looks up the fiber's stored state, finds the current value (which is 1 once the click has been processed) and returns that. The argument to useState is only an initial value: it is used on the mount render and ignored afterwards. From then on the fiber is the source of truth, not the literal in your function body.
This is the full answer to the opening puzzle. The count is not in the function, which forgets everything between calls, and not in the element, which is discarded after each render. It lives in the fiber, a separate object that React keeps and updates across every render of Counter.
Two practical consequences
This model explains a couple of behaviors that otherwise feel arbitrary:
- Changing the argument of
useStateafter mount has no effect on the stored value, because React only reads it the first time. If you need to reset state from props, change the component'skeyso React creates a new fiber. - Because the fiber is looked up by position in the tree, state belongs to where a component is rendered, not to the function definition. Two
<Counter />elements side by side get two fibers and two independent counts.
What the fiber holds beyond these two fields
type and memoizedState are enough to solve the state puzzle, but a real fiber carries much more. It keeps a reference to the DOM node it produced, pointers to its parent, first child and next sibling so React can walk the tree, and the previous props to compare with incoming ones. Those fields drive how React works out what changed and how it traverses a whole component tree, which is a separate problem from where a single value lives.
What can be pinned down now is the dividing line between the two objects, since blurring them is the source of much confusion about rendering:
Element Fiber
-------- -----
Created by React.createElement Created internally by React
New object every render Same object, updated in place
Discarded right after Persists for the component's
React reads it entire mounted lifetime
Holds no history Holds memoizedState, the real
remembered value across renders
Describes what should exist Is the thing that actually exists,
with real memory attached to it
A compact way to hold this in your head: an element is a request you make to React, while a fiber is React's own record of what currently exists. Each render produces a fresh batch of cheap, memoryless elements and hands them to the fiber tree, which was already there from the previous render and holds everything that must survive. One side describes, the other remembers.
One loose end: fibers come in pairs
The statement that a fiber is "updated in place" is a useful first approximation, but it is not the whole truth. React actually keeps two versions of each fiber, one matching what is currently on screen and one being prepared for the next update, and swaps between them. That arrangement is what lets React work on an update without disturbing the visible UI, and it deserves a separate explanation.
Key takeaways
- A function component's local variables are recreated on every call, so the function itself cannot store state.
- JSX compiles to calls that produce elements: plain, disposable descriptions rebuilt on every render.
- Fibers are plain objects that persist while a component is mounted;
typesays which component a fiber tracks. useStatereads and writes values through the fiber'smemoizedState, which in reality points to a list of hook objects matched by call order.- The initial value passed to
useStatematters only at mount; after that, the fiber is the source of truth.