What is State — React’s Memory Between Renders

React components re-render when their data changes. But regular JavaScript variables do not cause re-renders — when you write let count = 0; count++, the component function runs again on the next render and resets count to 0. State is React’s solution: a special kind of memory that persists between renders and triggers a re-render when updated. The useState hook gives a component state by storing a value in React’s internal memory and providing a setter function that both updates the value and schedules a re-render.

Why Regular Variables Don’t Work

// ❌ Wrong — regular variable resets every render
function Counter() {
    let count = 0;   // reset to 0 on every render!

    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={() => { count++; }}>
                Increment
            </button>
        </div>
    );
    // Clicking the button increments count, but React never re-renders
    // because no state changed — and even if it re-rendered, count would
    // be reset to 0 because the function body re-executes from scratch.
}

// ✓ Correct — useState persists between renders
import { useState } from "react";

function Counter() {
    const [count, setCount] = useState(0);
    // useState(0) returns [currentValue, setterFunction]
    // React stores 'count' in its own memory — it survives re-renders
    // Calling setCount() updates the stored value AND triggers re-render

    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={() => setCount(count + 1)}>Increment</button>
            <button onClick={() => setCount(0)}>Reset</button>
        </div>
    );
}
Note: State is local to the component instance. If you render <Counter /> twice on the same page, each has its own independent count state — clicking one does not affect the other. This is a fundamental property: React components are isolated by default. State is not shared between component instances unless you lift it to a common ancestor or use a global state store.
Tip: Think of state as a snapshot: when React renders a component, it captures the current state values as a snapshot. All event handlers in that render see the state values from that snapshot, not the latest state. This is why setCount(count + 1) called three times in one event handler still only increments by 1 — all three calls see the same snapshot value of count. To increment three times, use the functional form: setCount(prev => prev + 1) three times.
Warning: Never call useState inside loops, conditions, or nested functions. React tracks hooks by their call order — if a hook is called conditionally, the order can change between renders, causing React to map the wrong state to the wrong hook call. The rule is: always call hooks at the top level of a function component, never inside if blocks or for loops. ESLint’s eslint-plugin-react-hooks enforces this rule automatically.

State is Asynchronous — The Snapshot Mental Model

function LikeButton({ postId }) {
    const [likes, setLikes] = useState(0);

    // ❌ Wrong — three setLikes calls use the same snapshot value
    function handleTripleLike() {
        setLikes(likes + 1);   // schedules: set to 0+1 = 1
        setLikes(likes + 1);   // schedules: set to 0+1 = 1 (same snapshot!)
        setLikes(likes + 1);   // schedules: set to 0+1 = 1 (same snapshot!)
        // Result: likes = 1, not 3!
    }

    // ✓ Correct — functional form always uses the latest pending value
    function handleTripleLike() {
        setLikes((prev) => prev + 1);   // schedules: 0 → 1
        setLikes((prev) => prev + 1);   // schedules: 1 → 2
        setLikes((prev) => prev + 1);   // schedules: 2 → 3
        // Result: likes = 3 ✓
    }

    return (
        <button onClick={handleTripleLike}>
            ♥ {likes}
        </button>
    );
}

When to Use State vs Derived Values

function PostSearch({ posts }) {
    const [searchQuery, setSearchQuery] = useState("");
    // ✓ State: user input that triggers re-render when changed

    // ✓ Derived value — computed from state, NOT separate state
    const filteredPosts = posts.filter((p) =>
        p.title.toLowerCase().includes(searchQuery.toLowerCase())
    );
    // DON'T do: const [filteredPosts, setFilteredPosts] = useState([]);
    // Then keep it in sync with searchQuery — that's redundant state.
    // Just compute it during render from the source of truth (searchQuery).

    return (
        <div>
            <input
                value={searchQuery}
                onChange={(e) => setSearchQuery(e.target.value)}
                placeholder="Search posts..."
            />
            <PostList posts={filteredPosts} />
        </div>
    );
}

Common Mistakes

Mistake 1 — Calling useState conditionally

❌ Wrong — breaks React’s hook order rules:

function Component({ isLoggedIn }) {
    if (isLoggedIn) {
        const [name, setName] = useState("");   // NEVER inside if!
    }
}

✅ Correct — always at the top level:

function Component({ isLoggedIn }) {
    const [name, setName] = useState("");   // ✓ always called
    if (!isLoggedIn) return null;
}

Mistake 2 — Using state for values that can be derived

❌ Wrong — redundant state that must be kept in sync:

const [posts,         setPosts]         = useState([]);
const [publishedCount, setPublishedCount] = useState(0);   // redundant!
// Must update publishedCount every time posts changes

✅ Correct — derive it:

const [posts, setPosts] = useState([]);
const publishedCount = posts.filter(p => p.status === "published").length;   // ✓

Quick Reference

Task Code
Declare state const [value, setValue] = useState(initialValue)
Update state setValue(newValue)
Update based on prev setValue(prev => prev + 1)
Boolean toggle setIsOpen(prev => !prev)
Initial value (lazy) useState(() => expensiveComputation())
Derived value Compute in render body — no extra useState needed

🧠 Test Yourself

A component calls setCount(count + 1) three times in a single click handler. count starts at 0. What is the final value after the click?