React useState Explained: Make Your Page React to Change

React.js Full Course: Beginner to Advanced · Start Here · 9 min read · Updated

What happens on a click

  1. The person clicks the button.
  2. The handler calls setCount with a new value. It never changes count directly.
  3. React saves the new state and plans a new draw.
  4. React calls your component again. This time useState gives back the new value.
  5. The screen updates to show the new count.

So far our components show fixed data. Real pages change: a counter goes up, a menu opens, a message appears. To remember data that changes, a component needs state. In this lesson you will learn state with the useState hook, step by step, using small examples.

Why a normal variable does not work

Let us try to build a counter with a plain variable. It looks right, but it does not work:

function Counter() {
  let count = 0;

  function add() {
    count = count + 1;
    console.log(count);
  }

  return <button onClick={add}>Clicked {count} times</button>;
}

If you click, the console shows 1, 2, 3, but the button always says 0. There are two reasons. First, React does not know that count changed, so it does not draw the component again. Second, even if it did, a function component starts from the top each time it is drawn, so let count = 0 would run again and reset the number. We need a place where React can keep the value safe between draws, and where changing it tells React to draw again. That place is state.

The useState hook

A hook is a special function whose name starts with use. Hooks let a component use React features. useState gives a component a piece of state.

import { useState } from 'react';

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

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

Let us read the key line, const [count, setCount] = useState(0);:

  • useState(0) creates a piece of state and says its first value is 0.
  • It returns an array with two things. The first is the current value, which we call count. The second is a function to change it, which we call setCount.
  • We use array destructuring to give them names. You can choose any names, but the pattern [thing, setThing] is the usual way.

When you click, setCount(count + 1) tells React: “the new value is one more”. React stores it, draws the component again, and this time count is 1. The button shows “Clicked 1 times”. The same happens for each click.

What happens when state changes

  1. You call the setter function, for example setCount(5).
  2. React saves the new value.
  3. React calls your component function again (a re-render).
  4. This time useState gives back the new value.
  5. React compares the new result with the old one and updates the page where it is different.

The big idea: you never change the page yourself. You change the state, and the page follows.

State can be any type

State is not only for numbers. Here are a few common kinds:

const [name, setName] = useState('');            // text
const [isOpen, setIsOpen] = useState(false);     // true or false
const [items, setItems] = useState([]);          // a list
const [user, setUser] = useState(null);          // an object, or nothing yet
const [form, setForm] = useState({ email: '', age: 0 });  // an object

Example: a toggle

function Menu() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>
        {isOpen ? 'Close menu' : 'Open menu'}
      </button>
      {isOpen && (
        <ul>
          <li>Home</li>
          <li>About</li>
        </ul>
      )}
    </div>
  );
}

Each click flips isOpen between true and false. The text on the button and the list both depend on it. You wrote no code to show or hide anything by hand. You only described what to show for each value.

Example: a text box

function Hello() {
  const [name, setName] = useState('');

  return (
    <div>
      <input
        value={name}
        onChange={(event) => setName(event.target.value)}
        placeholder="Your name"
      />
      <p>Hello, {name || 'stranger'}!</p>
    </div>
  );
}

Every key press makes a change event. The handler reads the new text with event.target.value and puts it in state. The input shows what is in state through its value. This pattern is called a controlled input, and the forms lesson explains it fully.

The golden rule: never change state directly

State is read-only. You must always use the setter. If you write count = count + 1 or items.push('x'), React does not notice, and the screen does not change. For numbers and text this is easy. For arrays and objects, you must make a new copy and pass the copy to the setter.

// Add to an array: make a new array
setItems([...items, 'new item']);

// Remove from an array
setItems(items.filter((item) => item !== 'old item'));

// Change an object: copy it, then replace one field
setUser({ ...user, name: 'Asha' });

// Change one item in a list
setItems(items.map((item) => item.id === 3 ? { ...item, done: true } : item));

The three dots ... are called the spread syntax. They copy all the items of an array, or all the fields of an object, into a new one. React decides whether something changed by checking if the value is a new object, so a copy is required. This idea is called immutability, and it is one of the most important habits to learn.

State updates are not instant

Calling the setter does not change the variable in the code that is running right now. It asks React to draw again with the new value. This surprises many beginners:

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

  function handleClick() {
    setCount(count + 1);
    console.log(count); // still the OLD value
  }
  return <button onClick={handleClick}>{count}</button>;
}

The console shows the old number, because that variable belongs to the draw that is already finished. The new value arrives in the next draw.

There is a second problem. If you call setCount(count + 1) three times in a row, you get +1, not +3, because each call used the same old count. When the new value depends on the old value, pass a function. React gives it the latest value:

setCount((current) => current + 1);
setCount((current) => current + 1);
setCount((current) => current + 1); // now it really adds 3

Where should state live?

  • Each useState call belongs to one component. Two copies of Counter have two separate counts.
  • Keep state as small as you can. Do not store data you can work out from other data. For example, store the list, and calculate its length when you need it.
  • If two components need the same state, move it to their closest shared parent. Lesson 14 covers this.

Also remember the rules of hooks: call them only at the top level of a component, never inside an if, a loop, or a nested function. And call them only inside components or custom hooks. React depends on the order of the hook calls staying the same on every draw.

Common mistakes

  • Changing state directly (count++, items.push(...)).
  • Forgetting to copy an object or array before changing it.
  • Expecting the variable to change right after calling the setter.
  • Calling hooks inside conditions or loops.
  • Storing something you could calculate, which makes two values that can get out of step.

One more example: a like button

To finish, here is a small component that joins three ideas from this lesson: state, an event and a conditional message.

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

  function handleClick() {
    setLiked(!liked);
    setCount((c) => (liked ? c - 1 : c + 1));
  }

  return (
    <button onClick={handleClick} aria-pressed={liked}>
      {liked ? 'Liked' : 'Like'} ({count})
    </button>
  );
}

There are two pieces of state, and both change in one click. React waits until the handler finishes, and then draws once. The text on the button comes from state, and aria-pressed tells screen readers whether the button is on or off. Try to find the other places in your own projects where a small piece of state like this would help.

Your task

  1. Build a counter with three buttons: add one, subtract one, and reset to zero.
  2. Build a to-do list with an input and an Add button. Keep the list in state and add items using the spread syntax.
  3. Add a Remove button to each item using filter.

How to check it worked

  • The counter never goes wrong when you click quickly.
  • New to-do items appear at once and the input still works.
  • Removing an item removes only that item.

Summary

  • State is data that changes. useState(initial) returns the value and a setter.
  • A call to the setter makes React draw the component again with the new value.
  • Never change state directly. Make a new array or object with spread, map or filter.
  • If the new value depends on the old one, use setX((old) => ...).

Frequently asked questions

Why do I need useState instead of a normal variable?

A normal variable resets every time the component is drawn, and changing it does not tell React to draw again. State is kept safe between draws, and changing it with the setter makes React update the screen.

Why is my state not updating right after I set it?

The setter asks React to draw again with the new value. The variable in the running code still holds the old value. The new one is available in the next draw.

Can I have many useState calls in one component?

Yes. Use one for each separate piece of data, or use one object when the pieces always change together.

What is the difference between useState and a variable inside useEffect?

State is meant for data that the screen shows. A normal variable or a ref is better for values that do not need to cause a redraw.