Skip to main content

2.3 State Management with useState

← Back to Chapter 2: React Fundamentals

While props allow parents to pass data down, State is a component's personal memory. It holds data that changes over time based on user interactions, network responses, or timers, triggering React to re-render the component and update the screen.


1. Why Normal Variables Don't Work​

Consider a standard local variable inside a component:

function BrokenCounter() {
let count = 0;

function handleClick() {
count = count + 1; // Mutates local variable
console.log(count); // 1, 2, 3...
}

// React never re-renders! count on screen stays 0 forever.
return <button onClick={handleClick}>Count: {count}</button>;
}

Two problems prevent this from working:

  1. Local variables do not persist between renders: When React re-renders a component, it invokes the function from the top, resetting let count = 0.
  2. Changes to local variables do not trigger renders: React does not watch local variables; it only reacts to state setter calls.

2. The useState Hook​

The useState hook solves both problems:

  1. It creates a state variable that persists its value across re-renders.
  2. It provides a state updater function that updates the value and schedules a component re-render.
import { useState } from 'react';

function Counter() {
// [currentValue, updaterFunction] = useState(initialValue)
const [count, setCount] = useState(0);

return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}

3. Automatic Batching & Asynchronous Updates​

In React, calling setState does not immediately change the variable on the current line of execution. React schedules a state update and batches multiple state calls together for maximum performance:

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

function handleMultipleClicks() {
setCount(count + 1); // count is 0
setCount(count + 1); // count is STILL 0 in this render!
setCount(count + 1); // count is STILL 0 in this render!
// Result on next render: count will only be 1, NOT 3!
}

The Solution: Functional State Updates​

If you need to calculate the next state based on the previous state, always pass an updater function (prev => next):

function handleMultipleClicks() {
// React queues these updater functions and processes them in order:
setCount(prev => prev + 1); // 0 + 1 = 1
setCount(prev => prev + 1); // 1 + 1 = 2
setCount(prev => prev + 1); // 2 + 1 = 3
// Result on next render: count will accurately be 3!
}

4. State with Objects & Arrays​

When state holds an object or an array, you must never mutate it directly. Always pass a new copy using the spread operator:

Updating an Object​

const [profile, setProfile] = useState({ name: "Sarwvidya", age: 24 });

// ❌ WRONG: Mutates state directly (React will skip re-render!)
// profile.age = 25;
// setProfile(profile);

// ✅ CORRECT: Shallow copy with overrides
setProfile(prev => ({ ...prev, age: 25 }));

Updating an Array​

const [todos, setTodos] = useState(["Learn JS", "Learn React"]);

// ✅ Adding:
setTodos(prev => [...prev, "Master Redux"]);

// ✅ Removing:
setTodos(prev => prev.filter((_, index) => index !== 0));

// ✅ Modifying:
setTodos(prev => prev.map((todo, idx) => idx === 1 ? "Learn Next.js" : todo));

5. Summary Checklist​

  • State is component-specific memory that persists across renders and triggers UI updates.
  • useState returns a tuple containing the current state value and an updater function.
  • State updates are asynchronous and automatically batched by React for performance.
  • Use the functional updater form (prev => next) when the next state depends on the previous value.
  • Never mutate state objects or arrays directly; always return new cloned copies using spread syntax.