ReactState: A Component's Memory

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:

JSX
// ✗ 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, count would be reset to 0 at the top of the function.

Warning
Assigning to a local variable inside a component never causes a re-render. You must use `useState` (or another state mechanism) to make React aware of the change.
The Working Version with useState

JSX
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:

JSX
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 others
Note
This is different from a module-level variable, which is shared across all component instances. State is always per-instance.
State 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.

JSX
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 DOM
The 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.

JSX
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>
}
Tip
The snapshot behaviour often surprises beginners. If you need the most up-to-date state inside an async callback, use the functional update form: `setCount(prev => prev + 1)`. More on this in the "Updating State Correctly" page.
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)

Note
A good rule of thumb: if a variable does not affect what gets rendered, it probably does not need to be state. Use a ref (useRef) for values that need to persist between renders without triggering re-renders.