React

React useState Hook👨‍💻

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.

Key Takeaways

  • 1useState returns a `[value, setter]` tuple. The initial value is only used on the first render — after that, React tracks the state internally across re-renders.
  • 2State updates are asynchronous and batched. Calling `setCount(count + 1)` three times in a row results in a single increment, not three. Use functional updates (`prev => prev + 1`) when the new state depends on the old.
  • 3Objects and arrays must be updated immutably — spread to create a new reference, never mutate in place. React uses `Object.is()` to detect changes, so same reference means no re-render.
  • 4Lazy initialization (`useState(() => expensiveComputation())`) runs the function only on the first render. Use it when the initial value comes from localStorage, URL parsing, or any non-trivial computation.
  • 5If a value can be computed from existing state or props, do not put it in state. Derive it during render instead. Extra state variables mean extra opportunities for things to get out of sync.
  • 6When your state transitions get complex — multiple fields that must update together, or the next state depends on the action type — that's when useReducer becomes a better fit than useState.

Master react usestate hook

Take the React Basics course with hands-on lessons and challenges.

Examples

Shopping cart with add, update quantity, and remove

tsx

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.

Multi-field form with a single state object

tsx

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.

Toggle menu with lazy initialization from localStorage

tsx

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.

Filter and sort controls with derived results

tsx

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.

Paginated data with page state

tsx

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.

When to switch from useState to useReducer

tsx

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.

Common Mistakes

Mistake:

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'`

Fix:

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.

Mistake:

Storing derived values in state — keeping a `filteredList` state variable that you update via useEffect every time the source list or filter changes

Fix:

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.

Mistake:

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

Fix:

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.

Mistake:

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

Fix:

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.

Best Practices

  • Default to functional updates (`prev => newValue`) any time the new state depends on the previous state. It eliminates stale closure bugs and works correctly with React's batching. Reserve direct value updates for cases where you're setting a completely independent value.
  • Keep state minimal. If you can compute it from existing state or props, do not store it. Every extra state variable is another thing that can get out of sync. Derive during render, memoize with useMemo if needed.
  • Group related fields into a single object state (like form fields), but keep unrelated pieces in separate useState calls. Five boolean toggles that change independently should be five calls. Ten form fields that reset together should be one object.
  • Extract the initial state value into a constant outside the component when it's a non-trivial object. This prevents creating a new reference on every render and makes resets trivial: `setForm(INITIAL_STATE)`.
  • Use TypeScript generics to type your state when the initial value does not carry enough type information — `useState<User | null>(null)` instead of letting TypeScript infer the type as `null`.
  • Know when useState is not enough. If you have 3+ interdependent state variables, or state transitions that depend on action types (load/success/error), switch to useReducer. Forcing complex logic into multiple useState calls leads to impossible states and scattered update logic.

Summary

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.

Practice React with hands-on challenges

Learn react usestate hook hands-on in your IDE

Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.

Related Concepts

Related Cheatsheets

Master React with Stanza

Interactive lessons and challenges, right in your code editor.

Check the free courses. No credit card.