DevAcademy
LearnReactuseState & State
BeginnerReact

useState & State

Learn how to give a component memory that persists across renders using the useState hook.

Reading Time

18 min

Lesson

Lesson 8 of 42

Why Not Just Use a Regular Variable?

A regular variable inside a component resets every time the component re-renders, and changing it doesn’t trigger a re-render at all. State solves both problems: it persists between renders, and updating it tells React to re-render the component.

A Basic Counter with useState

import { useState } from "react";

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

  return (
    <button onClick={() => setCount(count + 1)}>
      Clicked {count} times
    </button>
  );
}

Reading the useState Return Value

useState(initialValue) returns an array with exactly two items: the current value, and a function to update it. Array destructuring is used to name them however you like.

useState Return Value

ItemPurpose
countThe current state value for this render
setCountA function that updates the state and schedules a re-render

State Updates Trigger Re-Renders

Calling the setter function tells React: "the state changed, please re-render this component (and its children) with the new value." The component function runs again from the top, returning new JSX based on the updated state.

Functional Updates

When a new state value depends on the previous one, pass a function to the setter instead of a plain value — this guarantees you’re always updating from the latest state, even if multiple updates are queued together.

Functional Update Form

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

  function handleTripleIncrement() {
    setCount((prev) => prev + 1);
    setCount((prev) => prev + 1);
    setCount((prev) => prev + 1);
  }

  return <button onClick={handleTripleIncrement}>Count: {count}</button>;
  // Correctly increments by 3, since each update reads the latest prev value
}

State Updates Are Asynchronous (Batched)

setCount(count + 1) doesn’t update count immediately — it schedules an update for the next render. Reading count right after calling the setter still shows the old value, which is why the functional update form matters for sequential updates.

Never Mutate State Directly

State must always be updated by calling its setter with a new value — mutating an object or array in place and expecting a re-render will not work, since React compares references to detect changes.

Updating Object State Immutably

function Profile() {
  const [user, setUser] = useState({ name: "Alice", age: 30 });

  function birthday() {
    // Wrong: user.age++; setUser(user); — mutates in place, won't trigger a re-render
    setUser({ ...user, age: user.age + 1 }); // Correct: a new object
  }

  return <button onClick={birthday}>{user.name} is {user.age}</button>;
}

Best Practice

Keep state as minimal as possible — don’t store anything in state that can be derived from other state or props during render. Fewer state variables means fewer places for state to get out of sync.

Interview Questions

Quick Quiz

1. What does useState(0) return?

2. Does calling the state setter update the variable immediately?

3. Why is setUser({ ...user, age: user.age + 1 }) correct, but mutating user.age directly is not?