State: A Component's Memory
Props let a parent configure a child, but they cannot change over time from the component's own perspective. What if a component needs to remember something between renders — a counter value, whether a menu is open, or the text a user typed? That is what state is for.
State is data that is owned by the component, can change over time, and causes the component to re-render when it changes. Think of it as the component's private memory.
Why Regular Variables Don't Work
A common mistake when learning React is trying to use a regular local variable as dynamic data. Let's see why that fails:
// ✗ BROKEN — this does NOT work
function Counter() {
let count = 0 // just a local variable
function increment() {
count = count + 1 // we change it...
console.log(count) // ...and the value is correct in the console
// but the UI never updates!
}
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+1</button>
</div>
)
}Two problems kill this approach:
React does not know the variable changed. Changing a local variable does not tell React to re-render the component. React only re-renders in response to state updates, prop changes, or parent re-renders.
Local variables are reset every render. Every time React calls your function component, it starts fresh. Even if a re-render happened,
countwould be reset to0at the top of the function.
The Working Version with useState
import { useState } from 'react'
// ✓ WORKS — state persists and triggers re-renders
function Counter() {
const [count, setCount] = useState(0)
function increment() {
setCount(count + 1) // tells React: please re-render with the new value
}
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+1</button>
</div>
)
}The useState hook does two things that a local variable cannot:
Persists the value between renders — React stores it outside the function so it survives each call
Triggers a re-render — calling the setter function (
setCount) tells React to call your component function again with the new value
State Is Private to the Component
State is fully encapsulated. Two instances of the same component each get their own independent state — they do not share values:
function App() {
return (
<div>
<Counter /> {/* has its own count, starts at 0 */}
<Counter /> {/* has its own count, starts at 0 — independent! */}
<Counter /> {/* has its own count, starts at 0 — independent! */}
</div>
)
}
// Clicking +1 on one Counter does NOT affect the othersState vs Props at a Glance
Props come from outside (the parent). State is managed inside the component.
Props are read-only. State can be changed with the setter function.
Props are controlled by the parent. State is controlled by the component itself.
Both props and state trigger a re-render when they change.
State Triggers Re-Renders
Every time you call a state setter, React schedules a re-render of that component (and its children). During the re-render, React calls your component function again and React computes what changed in the DOM and updates only those parts.
import { useState } from 'react'
function ToggleMessage() {
const [visible, setVisible] = useState(false)
return (
<div>
<button onClick={() => setVisible(!visible)}>
{visible ? 'Hide' : 'Show'} Message
</button>
{visible && <p>Hello! I appear and disappear.</p>}
</div>
)
}
// Each click:
// 1. setVisible(!visible) is called
// 2. React re-renders ToggleMessage with the new value of visible
// 3. React updates only what changed in the DOMThe Snapshot Mental Model
A useful way to think about state is as a snapshot. When React renders your component, it takes a snapshot of the current state values and hands them to your function. The JSX your function returns is a description of the UI for that state snapshot.
When state changes, React takes a new snapshot and re-renders. The old snapshot is discarded. This is why event handlers "see" the state value from the render in which they were created — not some live, mutable variable.
function AlertCounter() {
const [count, setCount] = useState(0)
function handleClick() {
setCount(count + 1)
// 'count' here is the SNAPSHOT from this render, not the updated value
// If count was 3, this alert will show 3, not 4:
setTimeout(() => alert(`Count was: ${count}`), 2000)
}
return <button onClick={handleClick}>Count: {count}</button>
}What Kinds of Data Should Live in State?
User input — text in a form field, selected options, toggle checkboxes
UI state — is a modal open? which tab is active? is the sidebar collapsed?
Fetched data — the result of an API call that changes over time
Derived computations — if a value can be computed from props or other state, it should NOT be state (compute it inline instead)