C.W.K.
Stream
Lesson 01 of 07 · published

useState — The Only State Hook You Need Most of the Time

~14 min · usestate, state, fundamentals

Level 0React Novice
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
useState is small. The traps around it are not. This lesson is the trap map.

The shape

const [value, setValue] = useState(initialValue). The hook returns a tuple of [current value, setter function]. The current value is fresh on every render. The setter is stable — its identity doesn't change across renders, so you can pass it to dependency arrays without it triggering re-runs.

The lazy initializer

If initialValue is expensive to compute, use a function form: useState(() => computeIt()). React calls the function once on the initial render and ignores it forever after. The bare-call form useState(computeIt()) runs the computation every render even though React only uses the result from the first call.

Functional updates

If your new value depends on the previous value, pass a function to the setter: setCount(n => n + 1). This protects you from stale-closure bugs (where an event handler captures an old value and another update lands between captures). Always use the functional form when the new value depends on the old one.

State batching

React 19 batches all state updates that happen within the same event handler, effect, or async function call. setA(1); setB(2); in a click handler triggers one re-render, not two. You don't usually have to think about this — but if you're surprised that two updates produced one render, batching is why.

Objects and arrays — immutable updates only

State updates by reference equality. setUser(user) with the same object reference is a no-op. To update an object field, spread to a new object: setUser({ ...user, name: 'new' }). Same for arrays: setItems([...items, newItem]). Mutating the existing object/array and calling the setter won't trigger a re-render.

Multiple useState vs one big object

If two state values change together, group them: useState({ x: 0, y: 0 }). If they change independently, separate them: useState(0); useState(0). The rule of thumb: state that always updates together belongs together; state that updates independently splits.

Derived state isn't state. If fullName can be computed from firstName and lastName, don't store it in useState. Compute it in render: const fullName = `${firstName} ${lastName}`. Storing derived values is how state-sync bugs start.

Code

useState — the four common shapes·tsx
import { useState } from "react";

export function Examples() {
  // 1. Simple scalar
  const [count, setCount] = useState(0);

  // 2. Lazy initializer — expensive computation runs once
  const [tree, setTree] = useState(() => buildTreeFromLocalStorage());

  // 3. Functional update — safe with batching and stale closures
  const increment = () => setCount((n) => n + 1);

  // 4. Object state — immutable updates
  const [user, setUser] = useState({ name: "", email: "" });
  const updateName = (name: string) => setUser((u) => ({ ...u, name }));

  return (
    <div>
      <button onClick={increment}>Count: {count}</button>
      <input
        value={user.name}
        onChange={(e) => updateName(e.target.value)}
      />
    </div>
  );
}

function buildTreeFromLocalStorage() { return { nodes: [] }; }
The stale-closure trap·tsx
// WRONG — three rapid clicks increment by one, not three.
function Broken() {
  const [count, setCount] = useState(0);
  const onClick = () => {
    setCount(count + 1);  // captures the count at render time
    setCount(count + 1);  // same captured count
    setCount(count + 1);  // same captured count
  };
  return <button onClick={onClick}>Count: {count}</button>;
}

// RIGHT — functional updates compose correctly.
function Working() {
  const [count, setCount] = useState(0);
  const onClick = () => {
    setCount((n) => n + 1);
    setCount((n) => n + 1);
    setCount((n) => n + 1);
  };
  return <button onClick={onClick}>Count: {count}</button>;
}

External links

Exercise

Build a <DraftPost /> component with a single object state { title: string; body: string; tags: string[] }. Provide inputs for title and body, a button to add a tag, and a button to remove a tag by index. Then refactor into three separate useState calls. Which version reads more clearly in this case? (Trick question — for unrelated fields, separate usually wins; for tightly coupled fields, group wins. Form what intuition you can.)
Hint
For tags array updates: setTags((tags) => [...tags, newTag]) to add, setTags((tags) => tags.filter((_, i) => i !== removeIdx)) to remove. Never mutate in place.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.