useState & State Management
useState & State Management
State is what makes a React component interactive. Without state, components are pure functions — same input always produces the same output. useState adds memory: a component can remember a value between renders and update the UI when that value changes.
Why Not Use a Regular Variable?
// This does NOT work — React does not know to re-render
function Counter() {
let count = 0 // ← regular variable, lost on every render
return (
<div>
<p>{count}</p>
<button onClick={() => { count++ }}>
Increment
</button>
</div>
)
}When you click the button, count increments but React never re-renders because nothing told it state changed. You need useState — it signals React to re-render the component with the new value.
Basic useState
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0) // initial value = 0
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(count - 1)}>-</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
)
}Every call to setCount schedules a re-render. React re-runs the function, useState returns the new value, and React updates the DOM to match the new JSX.
Functional Updates — When State Depends on Previous
// RISKY — count might be stale in async or batched scenarios
setCount(count + 1)
// SAFE — functional update always receives the latest value
setCount(prev => prev + 1)
// When calling setCount multiple times in one handler:
function addThree() {
setCount(prev => prev + 1) // ← each sees the result of the previous
setCount(prev => prev + 1)
setCount(prev => prev + 1)
// count increases by 3 — correct
}Use the functional pattern whenever the new state depends on the old value.
State with Objects — Spread to Update
interface FormState {
name: string
email: string
message: string
}
function ContactForm() {
const [form, setForm] = useState<FormState>({
name: '', email: '', message: ''
})
// Spread previous state, override only the changed field
const handleChange = (field: keyof FormState) =>
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>
setForm(prev => ({ ...prev, [field]: e.target.value }))
return (
<form>
<input value={form.name} onChange={handleChange('name')} placeholder="Name" />
<input value={form.email} onChange={handleChange('email')} placeholder="Email" />
<textarea value={form.message} onChange={handleChange('message')} />
</form>
)
}Never mutate the state object directly (form.name = 'Jordan'). Always create a new object with the spread operator.
State with Arrays — Always Create a New Array
const [items, setItems] = useState<string[]>([])
// Add an item
setItems(prev => [...prev, newItem])
// Remove an item
setItems(prev => prev.filter(item => item !== targetItem))
// Update an item
setItems(prev => prev.map(item =>
item.id === targetId ? { ...item, done: true } : item
))Never use push(), splice(), or direct index assignment on state arrays. These mutate the existing array — React compares references, not deep values, so a mutated array still looks like the same array and the component does not re-render.
Derived State — Compute From State, Not Duplicate It
// Bad — two pieces of state that must always be in sync
const [items, setItems] = useState([...])
const [count, setCount] = useState(0) // ← duplicate
// Good — derive count from items
const [items, setItems] = useState([...])
const count = items.length // ← computed on every render, always correctIf a value can be calculated from existing state, compute it during render — do not store it as separate state.