Skip to main content

🎛️ The useState Hook

Props let a component receive data. State lets a component remember data and change it over time. If props are the ingredients handed to a recipe, state is the oven temperature — a value the component owns and adjusts while it runs. The useState Hook is how a function component gets its own memory.

Week 4 · Day 2 (Tuesday: State and Lifecycle) · Lecture 1

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain what state is and how it differs from props
  • Call useState and destructure its [value, setValue] return pair
  • Trigger a re-render by updating state instead of mutating a variable
  • Update objects and arrays immutably with the spread operator
  • Use functional updates when new state depends on the previous state
  • Apply lazy initial state and obey the Rules of Hooks

Estimated Time: 70 minutes

Practice: Build an interactive counter, a live text mirror, and a to-do list — all driven by state.

In This Lesson

What Is State?

A plain variable inside a component is forgotten the instant the function finishes running. React calls your component function again on every render — so any let count = 0 resets to 0 each time. That's useless for a counter, a form, or anything that must persist between renders.

State is data a component owns that survives across renders and, when changed, tells React to re-run the component and update the screen. Think of a chameleon: its color (the UI) tracks its surroundings (the state). Change the state and the appearance follows automatically — you never touch the DOM by hand.

💡 Why not just a normal variable?

Two reasons. First, a local variable is wiped on every render, so it can't remember anything. Second, even a variable that survived wouldn't tell React that the screen needs updating. State solves both: React keeps the value between renders and re-renders when you change it through the setter.

Props vs State

Both hold data and both trigger re-renders, so beginners mix them up. The dividing line is ownership: props come from above and are read-only; state is owned and mutated within the component.

PropsState
Passed in from the parentDeclared and owned inside the component
Read-only — the child never reassigns themUpdated via its setter function
Like function parametersLike a value the function remembers between calls
Re-renders when the parent passes new valuesRe-renders when you call the setter

A useful test: "Can this component change this value on its own?" If yes, it's state. If the value only ever arrives from a parent, it's a prop.

Anatomy of useState

useState is a function React gives you. You call it with the initial value, and it hands back an array of exactly two things: the current value and a setter function to change it. We destructure that array immediately.

import { useState } from 'react';

function Counter() {
    // useState returns a pair: [currentValue, setterFunction]
    const [count, setCount] = useState(0); // 0 is the initial value

    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={() => setCount(count + 1)}>Click me</button>
        </div>
    );
}
Destructuring useState: current value on the left, setter on the right, initial value as the argument const [ count , setCount ] = useState( 0 ) count current value setCount updater function 0 initial value
The array positions matter, not the names — you could write [age, setAge]. The convention is x / setX.

⚠️ Never assign to the state variable directly

Writing count = count + 1 does nothing useful — it changes a local variable React doesn't watch, and the value is wiped on the next render anyway. Always go through the setter: setCount(count + 1). The setter is what tells React "re-render me."

The State & Re-render Cycle

Here is the loop that powers every interactive React app. A user does something, you call the setter, React schedules a re-render, your function runs again with the new state, and the screen updates. Then it waits for the next event.

graph TD A[Initial render
useState gives starting value] --> B[React shows the UI] B --> C{User interacts
click, type, submit} C --> D["You call setState(newValue)"] D --> E[React schedules a re-render] E --> F[Component function runs again
useState now returns the new value] F --> B

The critical mental shift: you never manually edit the DOM. You describe what the UI should look like for a given state, change the state, and React figures out the minimal DOM update. Your job is to keep state correct; rendering is React's job.

Everyday Examples

1. A counter

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

    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={() => setCount(count + 1)}>Increment</button>
            <button onClick={() => setCount(count - 1)}>Decrement</button>
            <button onClick={() => setCount(0)}>Reset</button>
        </div>
    );
}

2. A controlled text input

The input's value comes from state and every keystroke updates that state. This "controlled input" pattern makes React the single source of truth for what's typed.

function TextMirror() {
    const [text, setText] = useState('');

    return (
        <div>
            <input
                type="text"
                value={text}
                onChange={(e) => setText(e.target.value)}
                placeholder="Type something..."
            />
            <p>You typed: {text}</p>
            <p>Character count: {text.length}</p>
        </div>
    );
}

3. A boolean toggle

function ToggleSwitch() {
    const [isOn, setIsOn] = useState(false);

    return (
        <button onClick={() => setIsOn(!isOn)}>
            {isOn ? 'ON' : 'OFF'}
        </button>
    );
}

Multiple pieces of state

Call useState as many times as you need. Prefer several small state variables over one giant object — each is independent and easier to reason about.

function SignupForm() {
    const [name, setName]   = useState('');
    const [age, setAge]     = useState(0);
    const [email, setEmail] = useState('');

    return (
        <form>
            <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" />
            <input type="number" value={age}
                onChange={(e) => setAge(Number(e.target.value))} placeholder="Age" />
            <input type="email" value={email}
                onChange={(e) => setEmail(e.target.value)} placeholder="Email" />

            <p>{name} ({age}) — {email}</p>
        </form>
    );
}

💡 Tip: use Number(...), not parseInt

parseInt("") returns NaN, which shows up as "NaN" on screen. Number("") returns 0, a friendlier default for an empty number field.

Objects & Arrays: Update Immutably

State can hold objects and arrays, but there's one golden rule: never mutate them in place. React decides whether to re-render by comparing the reference of the new state to the old. If you edit the same object, the reference is unchanged and React sees "nothing changed."

⚠️ The classic mistake

// ❌ WRONG — mutates the existing object, same reference
const updateAge = () => {
    user.age = 31;   // React never sees a new object...
    setUser(user);   // ...so it skips the re-render
};

Instead, build a new object with the spread operator and change only what you need:

// ✅ CORRECT — new object, new reference, React re-renders
const updateAge = () => {
    setUser(prevUser => ({ ...prevUser, age: 31 }));
};

Updating an object in state

function ShoppingCart() {
    const [cart, setCart] = useState({ items: [], total: 0, discount: 0 });

    const addItem = (item) => {
        setCart(prev => ({
            ...prev,                          // copy every existing field
            items: [...prev.items, item],     // new array with the item appended
            total: prev.total + item.price,
        }));
    };

    return (
        <div>
            <p>Items: {cart.items.length} — Total: ${cart.total}</p>
            <button onClick={() => addItem({ id: 1, name: 'Book', price: 20 })}>
                Add Book ($20)
            </button>
        </div>
    );
}

Updating arrays without mutating

Reach for the non-mutating array methods. They return a brand-new array, exactly what React wants.

GoalUse this (returns new array)Avoid (mutates)
Add an item[...arr, item]arr.push(item)
Remove an itemarr.filter(x => x.id !== id)arr.splice(...)
Change one itemarr.map(x => ...)arr[i] = ...
function TodoList() {
    const [todos, setTodos] = useState([]);
    const [draft, setDraft] = useState('');

    const addTodo = () => {
        if (!draft.trim()) return;
        // Append with spread — a new array
        setTodos([...todos, { id: Date.now(), text: draft, done: false }]);
        setDraft('');
    };

    const toggle = (id) =>
        // map returns a new array; only the matching item is replaced
        setTodos(todos.map(t => t.id === id ? { ...t, done: !t.done } : t));

    const remove = (id) =>
        // filter returns a new array without the removed item
        setTodos(todos.filter(t => t.id !== id));

    return (
        <div>
            <input value={draft} onChange={(e) => setDraft(e.target.value)} placeholder="Add a todo" />
            <button onClick={addTodo}>Add</button>
            <ul>
                {todos.map(todo => (
                    <li key={todo.id}>
                        <input type="checkbox" checked={todo.done} onChange={() => toggle(todo.id)} />
                        <span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>{todo.text}</span>
                        <button onClick={() => remove(todo.id)}>Delete</button>
                    </li>
                ))}
            </ul>
        </div>
    );
}

💡 key is required in lists

Each item rendered from an array needs a stable, unique key so React can track which item is which across renders. Use a real id (todo.id), never the array index if items can be reordered or removed.

Functional Updates & Batching

When the next state is computed from the current state, pass a function to the setter instead of a value. React calls it with the guaranteed-latest state, sidestepping stale values.

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

    // ❌ Both reads see the SAME stale `count`, so this adds 1, not 2
    const addTwoWrong = () => {
        setCount(count + 1);
        setCount(count + 1);
    };

    // ✅ Each callback receives the latest value, so this adds 2
    const addTwoRight = () => {
        setCount(prev => prev + 1);
        setCount(prev => prev + 1);
    };

    return <button onClick={addTwoRight}>Count: {count}</button>;
}

Why? React batches multiple state updates in one event handler and only re-renders once. During that batch, count still holds the render-time value, so count + 1 reads the same number twice. The functional form prev => prev + 1 always operates on the freshest state React has.

✅ Rule of thumb

If your new value reads the old value — incrementing, toggling, appending — use the functional form setX(prev => ...). If you're setting a brand-new value that doesn't depend on the old one, passing the value directly is fine.

Lazy Init & Rules of Hooks

Lazy initial state

The argument to useState is only used on the first render, but if you write useState(expensiveCalc()), that function still runs on every render — its result is just ignored after the first. Pass a function instead and React calls it only once.

// ❌ expensiveCalc() runs on EVERY render (result ignored after the first)
const [data, setData] = useState(expensiveCalc());

// ✅ the function runs ONLY on the first render
const [data, setData] = useState(() => expensiveCalc());

// Handy for reading persisted values once:
const [count, setCount] = useState(() => {
    const saved = localStorage.getItem('count');
    return saved ? Number(saved) : 0;
});

The Rules of Hooks

Hooks rely on being called in the same order every render, so React can match each useState to its stored value. Two rules keep that order stable:

graph TD A[Rules of Hooks] --> B[1. Call Hooks at the top level] A --> C[2. Call Hooks only from React functions] B --> D[Not inside if / loops] B --> E[Not inside nested functions] C --> F[Function components] C --> G[Custom Hooks]
// ❌ WRONG — a Hook inside a condition changes call order
function Bad({ track }) {
    if (track) {
        const [count, setCount] = useState(0); // Error!
    }
}

// ✅ CORRECT — Hook at the top level, condition on the usage
function Good({ track }) {
    const [count, setCount] = useState(0);
    if (track) {
        // use count conditionally
    }
}

💡 Let the linter guard you

The eslint-plugin-react-hooks package (bundled with Create React App and Vite's React template) flags Rules-of-Hooks violations as you type. Keep it on — it catches these before they become runtime bugs.

Practice & Quiz

🏋️ Exercise 1: A like button

Goal: Build a LikeButton that shows a heart and a count. Clicking toggles "liked" and adjusts the count by one. Use two pieces of state.

function LikeButton() {
    // TODO: liked (boolean) and likes (number)
    // Clicking should flip liked and add/remove one like
}
💡 Hint

Keep liked as a boolean and likes as a number. On click, toggle liked with setLiked(l => !l) and change the count with a functional update whose direction depends on the current liked.

✅ Solution
function LikeButton() {
    const [liked, setLiked] = useState(false);
    const [likes, setLikes] = useState(0);

    const handleClick = () => {
        setLikes(prev => prev + (liked ? -1 : 1));
        setLiked(prev => !prev);
    };

    return (
        <button onClick={handleClick}>
            {liked ? '❤️' : '🤍'} {likes}
        </button>
    );
}

🏋️ Exercise 2: RGB color picker

Goal: Three sliders (0–255) for red, green, blue drive a live color swatch. Show the resulting rgb(...) string.

✅ Solution
function ColorPicker() {
    const [rgb, setRgb] = useState({ r: 128, g: 128, b: 128 });
    const set = (key) => (e) =>
        setRgb(prev => ({ ...prev, [key]: Number(e.target.value) }));

    const color = `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;

    return (
        <div>
            <div style={{ width: 120, height: 120, background: color }} />
            <p>{color}</p>
            {['r', 'g', 'b'].map(key => (
                <input key={key} type="range" min="0" max="255"
                    value={rgb[key]} onChange={set(key)} />
            ))}
        </div>
    );
}

🎯 Quick Quiz

Question 1: What does useState(0) return?

Question 2: Why must you update objects in state immutably (with a copy)?

Question 3: When should you use the functional form setX(prev => ...)?

Best Practices & Pitfalls

✅ Do

  • Split unrelated data into multiple useState calls
  • Copy objects/arrays with spread before setting them
  • Use functional updates when the new value reads the old one
  • Name the pair consistently: x / setX
  • Give lazy initializers as a function: useState(() => ...)

❌ Don't

  • Assign to the state variable directly (count = 5)
  • Mutate state objects/arrays with push, splice, or obj.x = ...
  • Call Hooks inside if, loops, or nested functions
  • Expect state to change synchronously right after setState — the new value shows up on the next render

⚠️ State updates aren't instant

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

function handleClick() {
    setCount(count + 1);
    console.log(count); // 0 — NOT 1! count is still the render-time value
}

Calling the setter schedules a re-render; it doesn't reassign count in the running function. Read the new value on the next render, not right after the call.

Summary

🎉 Key Takeaways

  • State is a component's own memory — it survives renders and triggers re-renders when changed
  • useState(initial) returns [value, setter]; only update through the setter
  • Update objects and arrays immutably with spread, map, and filter
  • Use the functional form setX(prev => ...) when the next value depends on the previous one
  • State updates are batched and asynchronous — the new value appears on the next render
  • Follow the Rules of Hooks: call them at the top level of a React function, in the same order every time

📚 Additional Resources

🚀 What's Next?

Your components can now remember and change data. But real apps also talk to the outside world — fetching data, setting timers, subscribing to events. That's the job of the next lesson: the useEffect Hook, where you'll learn to run side effects after render and clean them up properly.

🎉 You gave your components memory!

Every interactive React feature — counters, forms, toggles, lists — is just state changing over time. You've got the core of it now.