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:
- Local variables do not persist between renders: When React re-renders a component, it invokes the function from the top, resetting
let count = 0. - 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:
- It creates a state variable that persists its value across re-renders.
- 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.
-
useStatereturns 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.