State
An event handler could run. A JavaScript variable could change. But the button on the screen did not change with it.
That leaves the next hole:
if props come from outside, where does changing local data live?
Props are input carried by an element description. Events report that something happened after the UI exists. State is the missing piece between those two ideas: component-owned data that can change over time and cause React to render again.[1]
A variable is not enough #
We can start from the smallest version of the problem:
let count = 0;
function Counter() {
function handleClick() {
count += 1;
console.log('count variable:', count);
}
return (
<button onClick={handleClick}>
Count: {count}
</button>
);
}
root.render(<Counter />);
setTimeout(() => {
const button = mountNode.querySelector('button');
button.click();
setTimeout(() => {
console.log('button text:', button.textContent);
}, 0);
}, 20);
Waiting to run
Not run yet.
The handler runs, and the variable changes. But the rendered text still says Count: 0.
That gives us two constraints:
- the value must survive after the component returns
- changing the value must tell React to render again
An ordinary variable does the first part only if we put it outside the component. It still does not tell React that the visible description should be replaced.
So local state needs two things:
- a remembered value
- a way to request another render with a new value
The state pair #
React gives function components that pair through useState[2].
const { useState } = React;
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<button onClick={handleClick}>
Count: {count}
</button>
);
}
root.render(<Counter />);
setTimeout(() => {
const button = mountNode.querySelector('button');
button.click();
button.click();
setTimeout(() => {
console.log('button text:', button.textContent);
}, 0);
}, 20);
Waiting to run
Not run yet.
Now the UI changes.
This line is the new piece:
const [count, setCount] = useState(0);
useState(0) returns a pair:
countis the current state value for this rendersetCountis the function that asks React to store a new value and render again
The 0 is the initial value. It supplies the value for the first render.
So the smallest useful definition is:
state is remembered component data, and the setter is how a component requests a new render with updated data
Rendering again #
The state value does not change the existing React element object in place.
Instead, calling the setter gives React a new value to remember. React then calls the component again, and the next call reads the updated state value.
We can make that visible by logging during render:
const { useState } = React;
function Counter() {
const [count, setCount] = useState(0);
console.log('render with count:', count);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
root.render(<Counter />);
function clickAfterMount() {
const button = mountNode.querySelector('button');
if (!button) {
setTimeout(clickAfterMount, 5);
return;
}
button.click();
}
clickAfterMount();
Waiting to run
Not run yet.
That is the key difference from the ordinary variable.
The render function can finish. Later, an event handler can call the setter. React remembers the next value and runs the component again.
So the path now looks like this:
- React renders the component with the current state value.
- The rendered DOM receives an event.
- The event handler calls the state setter.
- React renders the component again with the updated state value.
- React updates the DOM to match the new description.
State connects an event after render to a later render.
The value is a snapshot #
There is a small but important trap here. The variable returned by useState belongs to the current render.
If we log it immediately after calling the setter, it still has the value from this render:
const { useState } = React;
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
console.log('count inside handler:', count);
}
return (
<button onClick={handleClick}>
Count: {count}
</button>
);
}
root.render(<Counter />);
setTimeout(() => {
mountNode.querySelector('button').click();
}, 20);
Waiting to run
Not run yet.
The handler still sees the count from the render that created that handler.
That does not mean React ignored the update. It means setCount(...) schedules the next state value; it does not rewrite the local count variable already being used by this function call.
So a render is like a snapshot:
- it receives the current props
- it reads the current state
- it returns a UI description for those values
- event handlers created during that render close over those same values
The next render gets the next state value.
Updating from the previous value #
The snapshot model exposes another hole.
What if we need to calculate the next state from the previous state more than once during the same event?
This attempt looks like it should add two:
const { useState } = React;
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
setCount(count + 1);
}
return (
<button onClick={handleClick}>
Count: {count}
</button>
);
}
root.render(<Counter />);
setTimeout(() => {
const button = mountNode.querySelector('button');
button.click();
setTimeout(() => {
console.log('button text:', button.textContent);
}, 0);
}, 20);
Waiting to run
Not run yet.
Both setter calls used the same count snapshot. If count was 0, both calls asked for 1.
When the next value depends on the previous value, React lets us pass an updater function instead[3]:
const { useState } = React;
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount((current) => current + 1);
setCount((current) => current + 1);
}
return (
<button onClick={handleClick}>
Count: {count}
</button>
);
}
root.render(<Counter />);
setTimeout(() => {
const button = mountNode.querySelector('button');
button.click();
setTimeout(() => {
console.log('button text:', button.textContent);
}, 0);
}, 20);
Waiting to run
Not run yet.
Now each update receives the latest pending value.
That gives us a useful rule:
when the next state depends on the previous state, use the updater function form
For a simple replacement, setCount(count + 1) is fine. For queued calculations from the previous value, setCount((current) => current + 1) is the safer shape.
Local to one rendered component #
State is not a module-level variable. It belongs to a particular rendered component position.
If we render the same component twice, each copy gets its own state:
const { useState } = React;
function Counter({ label }) {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount((current) => current + 1)}>
{label}: {count}
</button>
);
}
function App() {
return (
<div>
<Counter label="First" />
<Counter label="Second" />
</div>
);
}
root.render(<App />);
setTimeout(() => {
const buttons = mountNode.querySelectorAll('button');
buttons[0].click();
buttons[0].click();
buttons[1].click();
setTimeout(() => {
console.log(buttons[0].textContent);
console.log(buttons[1].textContent);
}, 0);
}, 20);
Waiting to run
Not run yet.
Both buttons use the same Counter function. They do not share the same count.
This is the difference between props and state:
- props are passed into a component by its parent
- state is owned by a particular rendered instance of the component
The parent can choose props. The component that calls useState owns its state.
The hook-shaped constraint #
React has to match each useState call with the same remembered state slot on every render. If the calls appear in different orders, React cannot reliably know which state value belongs to which call.
For now, the working shape is:
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return <button onClick={handleClick}>Count: {count}</button>;
}
Declare the state need first. Use it while building the returned UI. Update it from events or other later callbacks.
Filling the hole #
We can now answer the question left by events.
Events tell us that something happened after the UI was rendered. But an event handler by itself only runs code.
State gives that code a way to affect a later render:
- the component reads state during render
- the rendered output includes event handlers
- an event handler calls a setter
- React remembers the new state value
- React renders the component again
So changing local data lives in component state, not in ordinary render-time variables.
Final definition #
State is data remembered by React for a particular rendered component instance. A function component declares state with useState(initialValue), which returns the current value and a setter function.
Calling the setter asks React to remember a new value and render again.
Summary #
State fills the hole that events exposed:
- ordinary variables can change without making React render again
useStatereturns a current value and a setter- calling the setter requests a later render with a new state value
- each render sees a snapshot of props and state
- updater functions are useful when the next state depends on the previous state
- state is local to a rendered component instance
Notes
- The live demos in this article use the React 19.2.5 and react-dom 19.2.5 development modules. React may batch state updates, so examples that log immediately after setting state are written to show the render model rather than timing guarantees for every environment.