The useState Hook — Declaring and Updating State

The useState hook is how function components manage local state. It replaces the this.state and this.setState of class components with a cleaner, function-based API. Every interactive feature in the MERN Blog — loading indicators, form inputs, modal open/closed, toggle buttons, pagination — uses useState at its core. In this lesson you will master the complete useState API: declaring state, updating primitives and objects and arrays correctly, and understanding the asynchronous nature of state updates.

useState Syntax

import { useState } from 'react';

// useState returns a tuple: [currentValue, setterFunction]
const [count, setCount] = useState(0);
//      ↑          ↑              ↑
//  current    setter fn     initial value

// Naming convention: [noun, setNoun]
const [posts,       setPosts]       = useState([]);
const [loading,     setLoading]     = useState(true);
const [error,       setError]       = useState(null);
const [searchQuery, setSearchQuery] = useState('');
const [isOpen,      setIsOpen]      = useState(false);
Note: Hooks must be called at the top level of your component function — never inside loops, conditions, or nested functions. React relies on the order of hook calls to associate each hook with its state slot across re-renders. Calling useState inside an if statement would make the order unpredictable, breaking React’s internal bookkeeping. The ESLint rules-of-hooks plugin enforces this automatically — always have it enabled.
Tip: The initial value passed to useState is only used on the first render. On every subsequent render, React ignores the initial value argument and returns the current value instead. This is why useState(0) is fine even though the expression is “evaluated” on every render — React discards the result after the first mount.
Warning: State updates in React are asynchronous and batched. When you call a setter like setCount(count + 1), the component does not re-render immediately — React schedules the update. If you call the setter multiple times in the same event handler, React batches them and applies all updates in a single re-render. This is why reading state immediately after setting it returns the old value. Use the functional updater form (setCount(prev => prev + 1)) when the new value depends on the previous value.

Updating Primitive State

function LikeButton() {
  const [count,   setCount]   = useState(0);
  const [liked,   setLiked]   = useState(false);
  const [message, setMessage] = useState('');

  // ── Direct value ──────────────────────────────────────────────────────────
  const reset   = () => setCount(0);
  const disable = () => setLiked(false);

  // ── Value based on previous — use functional updater ──────────────────────
  const increment = () => setCount(prev => prev + 1);   // safe: reads latest value
  const toggle    = () => setLiked(prev => !prev);       // safe: reads latest value

  // ── Why functional updater matters ────────────────────────────────────────
  // Batching problem: if React batches these, count + 1 reads stale value twice
  setCount(count + 1); // ← count is still 0 from this render
  setCount(count + 1); // ← count is still 0 from this render → final: 1 not 2

  // Functional updater: each call receives the latest queued value
  setCount(prev => prev + 1); // prev = 0 → queues 1
  setCount(prev => prev + 1); // prev = 1 → queues 2 → final: 2 ✓

  return <button onClick={toggle}>{liked ? '❤️' : '🤍'} {count}</button>;
}

Updating Object State — Immutable Patterns

function PostEditor() {
  const [post, setPost] = useState({
    title:   '',
    body:    '',
    excerpt: '',
    tags:    [],
    published: false,
  });

  // ── Update one field — spread the rest ────────────────────────────────────
  // NEVER: post.title = 'New Title'; setPost(post) — mutates state directly!
  const updateTitle = (newTitle) => {
    setPost(prev => ({ ...prev, title: newTitle }));
  };

  // ── Update nested field ───────────────────────────────────────────────────
  // If post had nested: post.meta.keywords
  const updateKeywords = (keywords) => {
    setPost(prev => ({
      ...prev,
      meta: { ...prev.meta, keywords },  // spread inner object too
    }));
  };

  // ── Generic field updater for form inputs ─────────────────────────────────
  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    setPost(prev => ({
      ...prev,
      [name]: type === 'checkbox' ? checked : value,
    }));
  };

  return (
    <form>
      <input name="title"   value={post.title}   onChange={handleChange} />
      <textarea name="body" value={post.body}    onChange={handleChange} />
      <input name="published" type="checkbox"
             checked={post.published}            onChange={handleChange} />
    </form>
  );
}

Updating Array State — Immutable Patterns

function TagManager() {
  const [tags, setTags] = useState(['mern', 'react']);

  // ── Add an item ────────────────────────────────────────────────────────────
  const addTag = (newTag) => {
    if (!tags.includes(newTag)) {
      setTags(prev => [...prev, newTag]);   // spread + append — does NOT mutate prev
    }
  };

  // ── Remove an item ─────────────────────────────────────────────────────────
  const removeTag = (tagToRemove) => {
    setTags(prev => prev.filter(t => t !== tagToRemove));
  };

  // ── Update an item at a specific index ─────────────────────────────────────
  const updateTag = (index, newValue) => {
    setTags(prev => prev.map((t, i) => i === index ? newValue : t));
  };

  // ── Sort without mutating ─────────────────────────────────────────────────
  const sortTags = () => {
    setTags(prev => [...prev].sort()); // [...prev] creates a copy before sorting
  };

  return (
    <div>
      {tags.map((tag, i) => (
        <span key={tag}>
          #{tag}
          <button onClick={() => removeTag(tag)}>×</button>
        </span>
      ))}
    </div>
  );
}

Multiple State Variables vs One Object

// ── Option A: Multiple separate state variables (recommended for unrelated data)
const [posts,   setPosts]   = useState([]);
const [loading, setLoading] = useState(false);
const [error,   setError]   = useState(null);
const [page,    setPage]    = useState(1);

// ── Option B: One object (for tightly related data that updates together)
const [fetchState, setFetchState] = useState({
  data:    [],
  loading: false,
  error:   null,
});

// Updating one field — must spread the rest
setFetchState(prev => ({ ...prev, loading: true }));

// Rule of thumb:
// ✓ Separate variables: when values are independent and often updated separately
// ✓ One object: when values always change together (like loading + error + data)

Common Mistakes

Mistake 1 — Mutating state directly

❌ Wrong — mutating the array/object in place:

const [posts, setPosts] = useState([]);

// WRONG: push mutates the existing array — React does not detect the change
posts.push(newPost);
setPosts(posts); // same reference — React thinks nothing changed, no re-render!

✅ Correct — create a new array:

setPosts(prev => [...prev, newPost]); // new array reference → React re-renders ✓

Mistake 2 — Reading state immediately after setting it

❌ Wrong — state is not updated synchronously:

setCount(count + 1);
console.log(count); // still the OLD value — update is scheduled, not applied yet

✅ Correct — use the new value directly, or derive from the functional updater:

const newCount = count + 1;
setCount(newCount);
console.log(newCount); // the new value ✓

Mistake 3 — Not using functional updater for sequential updates

❌ Wrong — multiple setters in the same handler using the same stale value:

// If called twice rapidly (e.g. double-click):
setLikeCount(likeCount + 1); // likeCount = 5 → schedules 6
setLikeCount(likeCount + 1); // likeCount = 5 (still!) → schedules 6 again → final: 6 not 7

✅ Correct — functional updater always reads the latest queued value:

setLikeCount(prev => prev + 1); // prev = 5 → 6
setLikeCount(prev => prev + 1); // prev = 6 → 7 ✓

Quick Reference

Task Code
Declare state const [value, setValue] = useState(initialValue)
Update primitive setValue(newValue)
Update based on previous setValue(prev => prev + 1)
Toggle boolean setFlag(prev => !prev)
Update object field setObj(prev => ({ ...prev, field: val }))
Add to array setArr(prev => [...prev, item])
Remove from array setArr(prev => prev.filter(i => i !== item))
Update item in array setArr(prev => prev.map((i, idx) => idx === n ? newVal : i))
Reset to initial setValue(initialValue)

🧠 Test Yourself

You have const [posts, setPosts] = useState([]). To add a new post you write posts.push(newPost); setPosts(posts). The UI does not update even though the array now contains the new post. Why?