The useState Hook
useState is the most fundamental React hook. It lets you add reactive state to a function component — a value that persists between renders and causes the component to update whenever it changes. You will use it in virtually every React component you write.
Anatomy of useState
import { useState } from 'react'
// const [stateValue, setterFunction] = useState(initialValue)
const [count, setCount] = useState(0)useState returns an array of exactly two elements, and you always destructure them immediately:
count— the current state value. On the first render it equalsinitialValue(here0). On subsequent renders React provides the latest value.setCount— the setter function. Call it with a new value to schedule a re-render with that value. By convention, name itset+ the state variable name.0— the initial value. React uses this only on the very first render. After that the initial value is ignored completely.
A Counter — the Classic Example
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count - 1)}>-</button>
<button onClick={() => setCount(0)}>Reset</button>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
)
}useState with Different Types
useState accepts any JavaScript value as the initial state. Here are the most common patterns:
// Boolean — toggle state
const [isOpen, setIsOpen] = useState(false)
// Toggle: setIsOpen(!isOpen) or setIsOpen(prev => !prev)
// String — text input or selection
const [query, setQuery] = useState('')
const [theme, setTheme] = useState('light')
// Number — counters, pagination
const [page, setPage] = useState(1)
const [zoom, setZoom] = useState(100)
// Object — a form or a record
const [user, setUser] = useState({ name: '', email: '' })
// Update one field:
// setUser({ ...user, name: 'Alice' })
// Array — a list of items
const [todos, setTodos] = useState([])
// Add an item:
// setTodos([...todos, newTodo])Multiple State Variables
You can (and should) call useState multiple times for independent pieces of state. React tracks them by the order they are called — which is why hooks must always be called at the top level, never inside conditions or loops:
function SearchBar() {
const [query, setQuery] = useState('')
const [page, setPage] = useState(1)
const [isLoading, setLoading] = useState(false)
const [results, setResults] = useState([])
async function handleSearch(e) {
e.preventDefault()
setLoading(true)
setPage(1)
const data = await fetchResults(query)
setResults(data)
setLoading(false)
}
return (
<form onSubmit={handleSearch}>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Searching…' : 'Search'}
</button>
{results.map((r) => <p key={r.id}>{r.title}</p>)}
</form>
)
}Toggle Example
function ThemeToggle() {
const [isDark, setIsDark] = useState(false)
return (
<button
onClick={() => setIsDark((prev) => !prev)}
style={{
background: isDark ? '#1a1a1a' : '#f5f5f5',
color: isDark ? '#fff' : '#000',
padding: '8px 16px',
border: 'none',
borderRadius: 4,
cursor: 'pointer',
}}
>
Switch to {isDark ? 'Light' : 'Dark'} Mode
</button>
)
}Controlled Input Example
function NameInput() {
const [name, setName] = useState('')
return (
<div>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your name"
/>
<p>
{name
? `Hello, ${name}!`
: 'Start typing above…'}
</p>
</div>
)
}Lazy Initialization
If computing the initial state is expensive (reading from localStorage, parsing JSON, running a big calculation), pass a function instead of a value. React calls it once on the first render and ignores it afterwards:
// ✗ Called on every render even though the result is only used once:
const [settings, setSettings] = useState(JSON.parse(localStorage.getItem('settings')))
// ✓ Lazy initializer — the function is only called on the first render:
const [settings, setSettings] = useState(() => {
const saved = localStorage.getItem('settings')
return saved ? JSON.parse(saved) : { theme: 'light', language: 'en' }
})State Updates Are Asynchronous
Calling a setter does not immediately change the state variable. React batches updates and applies them before the next render. This means the variable still holds the old value for the remainder of the current event handler:
function Example() {
const [count, setCount] = useState(0)
function handleClick() {
setCount(count + 1)
console.log(count) // Still 0! The update hasn't been applied yet.
// React will re-render the component AFTER this function returns,
// and 'count' will be 1 in the next render.
}
return <button onClick={handleClick}>{count}</button>
}useState(initialValue)returns[currentValue, setterFn]The initial value is used only on the first render
Calling the setter schedules a re-render with the new value
Multiple
useStatecalls create independent state variablesPass a function to
useStatefor expensive initial computations (lazy init)State updates are batched and asynchronous — the variable keeps its old value until the next render