Skip to content

useState Hook

useState is the hook you'll use most often. It declares a state variable and a setter function, triggering re-renders when the value changes. This lesson covers every useState pattern you need in production.

Declaring State with useState

const [state, setState] = useState(initialValue) returns the current state and an updater. The initial value is used only on the first render. For expensive initialization, pass a function: useState(() => compute()).

React 18 batches multiple setState calls in the same event into one render. Updates are scheduled, not applied synchronously in the middle of your function.

const [count, setCount] = useState(0);

// Functional update
setCount(prev => prev + 1);

// Lazy init (runs once)
const [items] = useState(() => JSON.parse(localStorage.getItem('items') ?? '[]'));

Functional updates receive the previous state and avoid stale closure bugs.

useState Patterns

// Primitive
const [open, setOpen] = useState(false);

// Object — always spread
setUser(u => ({ ...u, name: 'Ada' }));

// Array — immutable ops
setTodos(t => [...t, newTodo]);
  • Use separate useState calls for unrelated values.
  • Merge object state immutably with spread syntax.
  • Functional updates when new state depends on old.
  • Lazy init function for costly first-render computation.

useState Quick Reference

Common useState patterns and gotchas.

Pattern Code
Declare const [x, setX] = useState(0)
Update setX(newValue)
Functional update setX(prev => prev + 1)
Lazy init useState(() => expensive())
Reset setX(initialValue)
Object merge setO(o => ({ ...o, key: val }))

Stale Closures and Functional Updates

Event handlers capturing old state values should use functional updates or include state in dependency arrays when using effects.

// Bug: count may be stale in rapid clicks
setCount(count + 1);

// Fix
setCount(c => c + 1);

Splitting vs Combining State

Split unrelated state into multiple useState calls. Combine into one object when updates always happen together. When state logic gets complex, upgrade to useReducer.

Common Mistakes

  • Mutating state objects directly.
  • Using stale state in closures without functional updates.
  • Initializing with undefined causing uncontrolled/controlled warnings.
  • One giant state object causing unnecessary re-renders.

Key Takeaways

  • useState returns [value, setter] for local component state.
  • Updates are batched and asynchronous in React 18+.
  • Use functional updates when depending on previous state.
  • Split state logically; use useReducer when transitions complexify.

Pro Tip

If you have more than three related setState calls in one handler, consider useReducer instead.