useState is the most fundamental React hook. You've used it hundreds of times. But do you know exactly when re-renders happen, or how React batches state updates?
Let's explore — with a live demo you can interact with directly.
The demo
Here's a simple counter built with useState. Go ahead and click the buttons:
This is a live React component embedded in an MDX post
That component is a real React component, rendered live on this page — not a screenshot, not an iframe.
How it works
The counter uses a basic useState pattern:
'use client'
import { useState } from 'react'
export function CounterDemo() {
const [count, setCount] = useState(0)
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(c => c - 1)}>−</button>
<button onClick={() => setCount(0)}>Reset</button>
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
)
}Key things to know about useState
1. The updater function
Notice we're using setCount(c => c + 1) rather than setCount(count + 1). This is the functional update form and it matters when updates are batched:
// ⚠️ Might not work as expected in concurrent features
setCount(count + 1)
setCount(count + 1) // still count + 1, not count + 2
// ✅ Always correct — reads the latest queued state
setCount(c => c + 1)
setCount(c => c + 1) // correctly becomes count + 22. State updates trigger re-renders
Every call to setCount schedules a re-render. React batches multiple setState calls that happen in the same event handler — since React 18, this also applies to async contexts.
3. State is per-component-instance
Each instance of CounterDemo has its own isolated state. If you rendered two <CounterDemo /> components on the same page, their counts would be completely independent.
When to reach for useReducer instead
useState is great for simple, independent values. Reach for useReducer when:
- State transitions are complex
- Next state depends on previous state in non-trivial ways
- You have multiple related state values that update together
type State = { count: number; history: number[] }
type Action = { type: 'increment' } | { type: 'decrement' } | { type: 'reset' }
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { count: state.count + 1, history: [...state.history, state.count + 1] }
case 'decrement':
return { count: state.count - 1, history: [...state.history, state.count - 1] }
case 'reset':
return { count: 0, history: [] }
}
}More interactive posts coming soon — each one with something you can poke at directly in the page.