React7 min read

React useState Hook — Complete Guide

Learn React useState from basics to advanced patterns. Covers state initialization, functional updates, objects in state, and common mistakes to avoid.

reacthooksuseStatestate-management

React useState Hook — Complete Guide

useState is the most fundamental React hook. It lets you add state to functional components — a value that persists across renders and triggers re-renders when updated.

Basic Usage

import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

useState(0) returns a pair:

  1. count — the current state value
  2. setCount — a function to update the state

When you call setCount, React re-renders the component with the new value.

Functional Updates

When the new state depends on the previous state, use a function:

// Risky — might use stale value
setCount(count + 1);

// Safe — always uses latest value
setCount(prevCount => prevCount + 1);

Why does this matter? If you call setCount multiple times in the same event handler, React batches updates. Without functional updates, you'd be using the same stale count value:

function handleTripleIncrement() {
  // Wrong — all three use the same `count` value
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1); // Only increments by 1!
  
  // Correct — each uses the latest value
  setCount(c => c + 1);
  setCount(c => c + 1);
  setCount(c => c + 1); // Increments by 3 ✓
}

Objects in State

When state is an object, you must create a new object (not mutate the existing one):

const [user, setUser] = useState({ name: "Rahul", age: 22 });

// Wrong — mutating state directly
user.name = "Priya";
setUser(user); // React won't re-render!

// Correct — create a new object
setUser({ ...user, name: "Priya" });

React uses reference equality to detect changes. If you pass the same object reference, React thinks nothing changed.

Nested Objects

const [form, setForm] = useState({
  personal: { name: "", email: "" },
  address: { city: "", state: "" }
});

// Update nested field
setForm(prev => ({
  ...prev,
  personal: { ...prev.personal, name: "Amit" }
}));

For deeply nested state, consider using useReducer instead.

Arrays in State

const [items, setItems] = useState(["Apple", "Banana"]);

// Add item
setItems([...items, "Cherry"]);

// Remove item
setItems(items.filter((_, index) => index !== 1));

// Update item
setItems(items.map((item, i) => i === 0 ? "Mango" : item));

Never use push, splice, or direct index assignment — these mutate the array.

Lazy Initialization

If computing the initial value is expensive, pass a function:

// Runs every render (wasteful)
const [data, setData] = useState(expensiveComputation());

// Runs only on first render (efficient)
const [data, setData] = useState(() => expensiveComputation());

Common use case — reading from localStorage:

const [theme, setTheme] = useState(() => {
  return localStorage.getItem("theme") || "dark";
});

Multiple State Variables vs One Object

Multiple variables (preferred for unrelated state):

const [name, setName] = useState("");
const [age, setAge] = useState(0);
const [isStudent, setIsStudent] = useState(true);

Single object (when fields are related):

const [form, setForm] = useState({
  name: "",
  email: "",
  password: ""
});

Rule of thumb: If fields change together, group them. If they change independently, separate them.

Common Mistakes

1. Forgetting State Updates Are Async

State doesn't update immediately after calling the setter:

setCount(5);
console.log(count); // Still the old value!

If you need the new value, use it in the next render or in a useEffect.

2. Setting State in a Loop Without Functional Updates

// Bug — only adds 1
for (let i = 0; i < 5; i++) {
  setCount(count + 1);
}

// Correct — adds 5
for (let i = 0; i < 5; i++) {
  setCount(c => c + 1);
}

3. Storing Derived Values in State

// Unnecessary — don't store computed values
const [items, setItems] = useState([]);
const [count, setCount] = useState(0); // ← redundant

// Just compute it
const count = items.length; // ← derived from items

When to Use useState vs useReducer

Use useStateUse useReducer
1-3 simple valuesComplex state logic
Independent valuesValues that depend on each other
Simple updatesMultiple actions modify state differently

Key Takeaways

  1. useState returns [value, setter] — call the setter to trigger re-render
  2. Use functional updates (setX(prev => ...)) when new state depends on old state
  3. Never mutate state directly — always create new objects/arrays
  4. Use lazy initialization for expensive computations
  5. Don't store derived values in state — compute them instead

🚀 Practice What You Learned

Apply these concepts with hands-on coding challenges: