We use cookies to enhance your experience on the site
CodeWorlds

Updating State -- Patterns and Pitfalls

Imagine that the navigation systems of your spaceship operate based on state data - speed, course, fuel level. Every update to these parameters must be precise, because even a small error can throw the ship out of orbit. In React, state updates follow similar rules - you must know the patterns for safe updates so your application behaves predictably.

Functional update vs direct value

When you call

setState
, you have two options. The first is to pass a new value directly:

1setCount(count + 1);

The second is a functional update -- passing a function that receives the previous state value:

1setCount(prev => prev + 1);

Why does this matter? React batches state updates (batching) and does not execute them immediately. If you call

setCount(count + 1)
multiple times in a single handler, each call sees the same value of
count
:

1function FuelGauge() {
2  const [fuel, setFuel] = useState(50);
3
4  const handleTripleBoost = () => {
5    // All three see the same value of fuel!
6    // Result: fuel will increase by only 10, not 30
7    setFuel(fuel + 10);
8    setFuel(fuel + 10);
9    setFuel(fuel + 10);
10  };
11
12  const handleTripleBoostCorrect = () => {
13    // Each call receives the current value
14    // Result: fuel will increase by 30
15    setFuel(prev => prev + 10);
16    setFuel(prev => prev + 10);
17    setFuel(prev => prev + 10);
18  };
19
20  return (
21    <div>
22      <p>Fuel: {fuel}%</p>
23      <button onClick={handleTripleBoost}>Boost (incorrect)</button>
24      <button onClick={handleTripleBoostCorrect}>Boost (correct)</button>
25    </div>
26  );
27}

Rule: Always use functional updates (

prev => ...
) when the new value depends on the previous one.

Multiple state variables vs a single object

You have two strategies for storing state in a component. The first is separate state variables:

1function ShipDashboard() {
2  const [speed, setSpeed] = useState(0);
3  const [altitude, setAltitude] = useState(0);
4  const [heading, setHeading] = useState(0);
5
6  // Each variable updated independently
7  const accelerate = () => setSpeed(prev => prev + 10);
8  const climb = () => setAltitude(prev => prev + 100);
9
10  return (
11    <div>
12      <p>Speed: {speed} km/s | Altitude: {altitude} km | Heading: {heading}°</p>
13      <button onClick={accelerate}>Accelerate</button>
14      <button onClick={climb}>Climb</button>
15    </div>
16  );
17}

The second is state in an object -- useful when the data is related:

1function ShipDashboard() {
2  const [navigation, setNavigation] = useState({
3    speed: 0,
4    altitude: 0,
5    heading: 0
6  });
7
8  // IMPORTANT: always copy the entire object with the spread operator!
9  const accelerate = () => {
10    setNavigation(prev => ({ ...prev, speed: prev.speed + 10 }));
11  };
12
13  return (
14    <div>
15      <p>Speed: {navigation.speed} km/s</p>
16      <button onClick={accelerate}>Accelerate</button>
17    </div>
18  );
19}

When to use which?

  • Separate
    useState
    -- when values change independently of each other
  • Object in
    useState
    -- when values logically belong together (e.g., a form, x/y position)

Most common mistakes -- state mutation

In React, state is immutable. Never modify state directly -- always create a new copy. It is like a safety rule on a spaceship: you do not edit the ship's log, you just add a new entry.

Mistake 1: push instead of spread for arrays

1function MissionLog() {
2  const [logs, setLogs] = useState(["Mission started"]);
3
4  const addLog = (entry) => {
5    // MISTAKE: push mutates the existing array!
6    // React will not detect the change and will not re-render the component
7    logs.push(entry);
8    setLogs(logs);
9  };
10
11  const addLogCorrect = (entry) => {
12    // Spread creates a new array
13    setLogs(prev => [...prev, entry]);
14  };
15
16  return (
17    <div>
18      <button onClick={() => addLogCorrect("New entry")}>
19        Add Entry
20      </button>
21      {logs.map((log, i) => <p key={i}>{log}</p>)}
22    </div>
23  );
24}

Mistake 2: directly changing object properties

1function CrewMember() {
2  const [member, setMember] = useState({
3    name: "Captain Nova",
4    rank: "Captain",
5    missions: 12
6  });
7
8  const promote = () => {
9    // MISTAKE: direct mutation of the object
10    member.rank = "Admiral";
11    setMember(member);
12  };
13
14  const promoteCorrect = () => {
15    // Creating a new object with the spread operator
16    setMember(prev => ({ ...prev, rank: "Admiral" }));
17  };
18
19  return (
20    <div>
21      <p>{member.name}{member.rank}</p>
22      <button onClick={promoteCorrect}>Promote</button>
23    </div>
24  );
25}

Stale closures -- the closure trap

A closure is a situation where a function "remembers" variables from the moment it was created. In React, this can lead to reading outdated state:

1function CountdownTimer() {
2  const [seconds, setSeconds] = useState(10);
3
4  const startCountdown = () => {
5    // seconds is "frozen" from the moment of the click!
6    // Every interval sees the same value
7    setInterval(() => {
8      setSeconds(seconds - 1); // always 10 - 1 = 9
9    }, 1000);
10  };
11
12  const startCountdownCorrect = () => {
13    // Functional update always uses the current value
14    setInterval(() => {
15      setSeconds(prev => prev - 1);
16    }, 1000);
17  };
18
19  return (
20    <div>
21      <p>Countdown: {seconds}s</p>
22      <button onClick={startCountdownCorrect}>Start</button>
23    </div>
24  );
25}

Batching -- grouping updates

React 18+ automatically batches multiple state updates in a single event handler, performing only one re-render. It is like the onboard computer that collects all sensor reports and updates the display once, instead of refreshing it after each sensor:

1function NavigationUpdate() {
2  const [position, setPosition] = useState({ x: 0, y: 0 });
3  const [velocity, setVelocity] = useState(0);
4  const [status, setStatus] = useState("idle");
5
6  const handleJump = () => {
7    // React batches these 3 updates into one render
8    setPosition({ x: 100, y: 200 });
9    setVelocity(9999);
10    setStatus("hyperjump");
11    // The component renders ONCE, not three times!
12  };
13
14  return (
15    <div>
16      <p>Position: ({position.x}, {position.y})</p>
17      <p>Velocity: {velocity} | Status: {status}</p>
18      <button onClick={handleJump}>Hyperspace Jump</button>
19    </div>
20  );
21}

Practical state update patterns

Here are some patterns that will come in handy on board the React ship:

1// Removing an element from an array
2setItems(prev => prev.filter(item => item.id !== idToRemove));
3
4// Updating one element in an array
5setItems(prev => prev.map(item =>
6  item.id === targetId ? { ...item, status: "done" } : item
7));
8
9// Toggle (switching a boolean)
10setIsActive(prev => !prev);
11
12// Adding an element to the beginning of an array
13setMessages(prev => [newMessage, ...prev]);
14
15// Incrementing with a limit
16setFuel(prev => Math.min(prev + 10, 100));

Summary

Updating state in React -- key principles:

  1. Functional update (
    prev => prev + 1
    ) -- use when the new value depends on the previous one
  2. Never mutate state -- always create new arrays/objects (spread operator)
  3. Batching -- React groups updates and renders the component once
  4. Stale closures -- in callbacks and timers, use functional updates
  5. Multiple useState vs object -- depends on the logical relationship of the data

Remember: State in React is like the ship's log -- you never edit it, you just create a new entry based on the previous one!

Go to CodeWorlds