useState is how you make React components remember things between renders. It returns a value and a setter function — call the setter, React re-renders with the new value. That's the entire mental model, but the details of how you structure state, update objects immutably, and avoid stale closures are where most bugs live.
Master react usestate hook
Take the React Basics course with hands-on lessons and challenges.
The cart uses functional updates throughout because every operation depends on the previous state. addItem either increments an existing item's quantity or appends a new one. The total is derived during render — storing it in a separate state variable would be a bug waiting to happen.
Related fields that always travel together belong in one object. The generic handleChange function uses keyof to stay type-safe while avoiding a separate handler per field. Validation is derived during render, not stored in state.
The arrow function passed to useState is lazy initialization. Without it, JSON.parse would run on every render (the result gets thrown away after the first one, but the work still happens). The toggle uses a functional update because it inverts the previous value.
Each filter control is its own state variable because they change independently. The filtered/sorted product list is derived via useMemo, not stored in a separate state. Storing the filtered result in state would create a sync bug every time the source products prop changes.
Page number is state because the user controls it. The fetched data is also state because it comes from an external source. The cancelled flag in the cleanup prevents race conditions when the user clicks through pages quickly. Note that the disabled checks on the buttons are derived from existing state — no separate hasNext/hasPrev state needed.
The signal to switch: if you have 3+ state variables that must update together (like setting status and error in the same action), or if the next state depends on the action type rather than a simple value. useReducer makes these transitions explicit and keeps impossible states unrepresentable.
Mutating objects or arrays in state instead of creating new references — using push, splice, sort in-place, or direct property assignment like `user.name = 'Bob'`
Always create a new reference. Use spread for objects (`{ ...user, name: 'Bob' }`), and `map`, `filter`, `concat`, `toSorted()` for arrays. React compares references with `Object.is()` — same reference means it skips the re-render entirely.
Storing derived values in state — keeping a `filteredList` state variable that you update via useEffect every time the source list or filter changes
Compute derived values during render: `const filtered = items.filter(i => i.active)`. If the computation is expensive, wrap it in `useMemo`. A second state variable for derived data is a sync bug waiting to happen because you now have two sources of truth.
Using direct updates when the new state depends on the previous value — writing `setCount(count + 1)` in places where multiple updates can batch or closures can go stale
Use functional updates: `setCount(prev => prev + 1)`. This guarantees each update sees the latest pending state, not the value captured in the closure. Make it your default whenever the new value is a function of the old one.
Passing an expensive expression directly to useState — like `useState(JSON.parse(localStorage.getItem('data')))` — causing the computation to run on every render even though only the first result is used
Wrap it in a function for lazy initialization: `useState(() => JSON.parse(localStorage.getItem('data')))`. React only calls the function on the first render. This matters for anything involving parsing, deep cloning, or reading from storage.
useState gives components memory across renders. Call it with an initial value (or a function for lazy initialization), and you get back the current value plus a setter. Update objects and arrays immutably with spread syntax, use functional updates when the new state depends on the old, and keep your state minimal by deriving everything you can during render. When your state transitions outgrow simple setter calls, graduate to useReducer.
Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.
Interactive lessons and challenges, right in your code editor.
Check the free courses. No credit card.