Skip to main content

What State Really Is

What you'll learn

  • What state physically is, and where it lives while your function isn't running
  • Why two <Counter /> instances each get their own independent state
  • Why module-level variables fail as a replacement for state
  • When state survives a re-render, and when React throws it away
  • The snapshot insight: state never changes during a render

Here is a puzzle worth solving before anything else in this part of the tutorial. Every time React re-renders a component, it calls your function again, from the top. Every let and const inside is created fresh and thrown away when the function returns. Yet useState somehow "remembers" the count between renders. Who remembers? Where is the number actually kept while your function isn't running?

The answer is the foundational idea of this entire part:

Your component function is stateless. React stores the memory for you, and it files that memory under your component's position in the tree, not under your function.

Local variables can't do the job

Imagine trying to write a counter with a plain local variable:

function BrokenCounter() {
let count = 0;

function handleClick() {
count = count + 1;
console.log('count is now', count);
}

return <button onClick={handleClick}>Count: {count}</button>;
}

What happens:

  1. First render: count is 0, the button shows "Count: 0".
  2. You click. The handler reassigns the local variable, the log even says count is now 1. But nothing tells React anything happened, so no re-render occurs. The screen still shows 0.
  3. Even if something else triggered a re-render, the function would run from the top and reset count to 0 again.

Local variables fail at both jobs: they don't persist across renders, and changing them doesn't schedule a render.

State is memory React stores for you

useState fixes both halves:

import { useState } from 'react';

function Counter() {
const [count, setCount] = useState(0);

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

return <button onClick={handleClick}>Count: {count}</button>;
}

What happens:

  1. First render: React calls Counter(). The useState(0) call is really a question, "do you have a memory cell for this component?" React creates one, stores 0 in it, and hands back the current value plus a setter.
  2. Click: setCount doesn't touch your function. It writes a requested new value into React's cell and schedules a re-render. (The next chapter is entirely about this step.)
  3. Re-render: Counter() runs again from the top. useState asks the same question, this time React finds the existing cell and returns its stored value.
  4. Your function receives the remembered value as an ordinary constant and describes the UI with it.

Jargon: "state". Data that React stores on a component's behalf, that survives re-renders, and that can only be changed through its setter, which schedules a re-render.

So where does the cell physically live? On React's internal record for your component instance. React builds a node for every component in the tree, and that node carries the component's type, its props, and its state memory. You'll meet this node by name, the fiber, in the fiber chapter of this series; for now, picture a file card React keeps per component instance.

Jargon: "fiber". React's internal node representing one component (or host element) instance in the tree: it stores type, props, state memory, and links to neighbors. You never touch it directly.

Pseudocode model, not real source:

// Conceptually, React keeps one internal node per component instance:
const internalNode = {
type: Counter, // which function to call
props: {}, // latest props
stateCells: [ // the memory useState reads and writes
{ value: 0, setter: someInternalFunction },
],
child: someNode, // position in the tree
sibling: anotherNode,
};

The crucial detail: the memory lives on the node, not on Counter. The function is just a recipe.

The memory is tied to position, not to the function

Render the same component twice:

import { useState } from 'react';

function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}

function App() {
return (
<div>
<Counter />
<Counter />
</div>
);
}

What happens:

  1. Rendering App produces two Counter elements, so React creates two internal nodes, call them node A and node B.
  2. Each node gets its own state cell, both initialized to 0.
  3. You click the first button. The setter that render received belongs to node A's cell, so the update lands on node A.
  4. React re-renders: node A's Counter receives 1, node B's still receives 0.
  5. The two counters tick independently forever.

One function, two nodes, two states. When people say "the same component", what React actually tracks is "the same position in the tree, with the same type". That pair, position plus type, is the identity the memory is filed under.

Why module-level variables fail

If state is "just memory", why not a variable outside the component?

let globalCount = 0; // module-level: one variable for the whole app

function BadCounter() {
return (
<button onClick={() => { globalCount = globalCount + 1; }}>
Count: {globalCount}
</button>
);
}

What happens:

  1. You click: globalCount really does increment, but nothing schedules a render, so the screen never updates. Module variables have no setter that talks to React.
  2. Even if some other update forced a render, every <BadCounter /> instance on the page would read the same globalCount. There is one variable, shared by all instances, no per-instance memory is possible.
  3. And when the component unmounts, the variable stays behind, leaking data that should have died with the UI.

That is precisely the two jobs useState does for you: private per-instance memory, plus a setter that schedules a render. Module variables provide neither.

State survives re-renders, but dies on unmount

Watch state get destroyed by rendering a counter conditionally:

import { useState } from 'react';

function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}

function App() {
const [show, setShow] = useState(true);
return (
<div>
<label>
<input
type="checkbox"
checked={show}
onChange={e => setShow(e.target.checked)}
/>
Show counter
</label>
{show && <Counter />}
</div>
);
}

What happens:

  1. Mount with show = true: React creates a node for Counter, cell set to 0.
  2. Click up to 5: the cell holds 5. Re-renders come and go; the cell survives them all.
  3. Uncheck the box: show becomes false, and the element tree no longer contains a Counter at that position. React removes the node, and its cell, entirely. Not hidden. Discarded.
  4. Re-check: a brand-new node appears at that position, useState(0) initializes it fresh, and the counter shows 0.
  5. The 5 is unrecoverable. Unmounting is the end of state.

The flip side is the rule you met in the diffing chapters: same position + same type = the node (and its state) is kept; different type at that position = old node discarded, new node built. If the checkbox example rendered a <Timer /> instead of a <Counter /> at that spot, the state would reset for exactly the same reason, React would be looking at a different component. State retention is just the diffing identity rule, seen from the inside.

The snapshot insight: state is a constant within one render

One more consequence, and it's the one that will carry the next chapter:

import { useState } from 'react';

function Counter() {
const [count, setCount] = useState(0);
console.log('Render: count =', count);

return (
<button onClick={() => {
setCount(count + 1);
console.log('Handler: count =', count);
}}>
Count: {count}
</button>
);
}

What happens:

  1. Render: count is the constant 0 for this entire function execution. Log: Render: count = 0.
  2. Click: the handler runs, still inside that render's world. It logs Handler: count = 0, then requests 1.
  3. count does not "become 1" during the handler, it's a const, bound to 0 for the whole render. It can never change within this run.
  4. The next render is a fresh function call with a fresh constant, this time bound to 1.

State doesn't change during a render. React swaps the value between renders. Each render is a snapshot: the props, the state values, and every handler defined inside all belong to that one moment. Keep this sentence in your pocket, it dissolves half of all state bugs.

Diagram

Rendered diagram (PNG hi-res):

rendered diagram

Rendered diagram (PNG hi-res):

rendered diagram

Common misconceptions

  • Misconception: State lives inside the component function. Reality: the function is stateless; the memory lives on React's internal node and is handed in during each render.
  • Misconception: Two instances of the same component share state. Reality: each position in the tree gets its own node and its own cells.
  • Misconception: useState creates a special variable that updates itself. Reality: each render gets an ordinary constant; React swaps the value between renders.
  • Misconception: A module-level variable is a lightweight alternative to state. Reality: it's shared across all instances and never schedules a render.
  • Misconception: Hiding a component preserves its state for when it returns. Reality: unmounting destroys the node and its cells; remounting starts from the initial value.
  • Misconception: Setting state re-runs just the line that changed. Reality: the whole function re-runs, top to bottom, with the new value.

Why it works this way

  • Tree position is a natural identity. The UI is a tree; "the counter on the left" vs "the counter on the right" is exactly how you already think. React uses position + type as the filing system for memory.
  • Stateless functions are predictable. Output depends only on props plus the stored cells. Such functions are easy to replay, pause, test, and even run on a server.
  • An external memory store keeps renderers swappable. The same component works for DOM, native, or test renderers, because the state model doesn't assume any platform.
  • Snapshot semantics keep a render consistent. Within one render, the UI can't be half-old, half-new, the values it describes come from a single moment.
  • Discarding state on unmount matches intuition and budgets. Things that leave the screen lose their temporary data unless you deliberately keep it elsewhere (lifting state up), and memory stays bounded.

Try it yourself

  1. Render two <Counter /> instances side by side. Click one several times, then the other. Confirm they never affect each other, two nodes, two cells.
  2. Build the checkbox example. Count up to 5, uncheck, re-check. Watch the count return to 0: the node was destroyed and rebuilt.
  3. Add console.log('render', count) at the top of Counter and console.log('handler', count) in the click handler. Click twice, slowly. Notice the handler always logs the value from its own render, never a live value.
  4. Try the module-level variable version. Click and watch the screen stay frozen. Then render two <BadCounter /> instances and trigger a render from elsewhere, both show the same shared number.

Recap

  • Component functions are stateless, local variables reset on every render.
  • State is memory React stores for you, on the internal node (fiber) at your component's position in the tree.
  • Identity = position + type. Two instances = two nodes = fully independent state.
  • Module variables fail on both fronts: shared across instances, and they never schedule a render.
  • State survives re-renders but is destroyed on unmount; remounting starts fresh from the initial value.
  • Within one render, state is an ordinary constant, a snapshot. React swaps values between renders, never during.

Next

setState is a request, not a change →