Himanshu SharmaSoftware engineer
Let’s talk

React · Lesson 14 of 38

State with useState

Make React components remember values with useState: why plain variables fail, per-instance state, lazy initial values and the rules of hooks.

18 minState and eventsUpdated 24 October 2026

By the end of this lessonAdd state to a component with useState, update it from event handlers, and explain why a normal variable would not work.

Props let a parent give data to a component. But components also need memory of their own: the number of times a button was clicked, the text typed into a field, whether a menu is open. In React that memory is called state, and it is the feature that turns a static page into an interactive one.

Why a normal variable is not enough

Your first idea for a counter might be an ordinary variable. Try it:

jsx
function App() {
  let count = 0;

  function handleClick() {
    count = count + 1;
    console.log("count is now", count);
  }

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

export default App;
result
Clicked 0 times
CONSOLE
count is now 1
count is now 2
count is now 3

The Console shows count changing, but the button text always stays at 0. There are two reasons, and both matter:

  1. A local variable does not survive. Each time React renders the component, the function runs from the top, and let count = 0 creates a fresh variable. Whatever you changed last time is gone.
  2. Changing a variable does not tell React to re-render. React only updates the screen when it is told that something it cares about has changed.

State solves both: it is stored by React outside your function so it survives renders, and the update function that changes it also schedules a new render.

useState

useState is a hook, a special function from React whose name starts with use. You call it inside the component with an initial value:

jsx
import { useState } from "react";

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

  function handleClick() {
    setCount(count + 1);
  }

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

export default App;
result
Clicked 3 times

Now it works: three clicks, "Clicked 3 times". What const [count, setCount] = useState(0) means:

  • useState(0) asks React for a piece of state whose first value is 0. The 0 is used only on the very first render; afterwards React remembers the current value.
  • It returns an array of exactly two items. You pick names with array destructuring: count is the current value, and setCount is the function that replaces it. The convention is [thing, setThing].
  • Calling setCount(newValue) asks React to store the new value and render this component again. The new render sees the new count.

The sequence for a click is: the handler runs, calls setCount(count + 1), React calls App() again, this time useState returns 1, and React updates the button text to "Clicked 1 times". You never touch the page yourself.

State is private to each instance

State belongs to one instance of a component in the tree. Two buttons made from the same component remember separately:

jsx
import { useState } from "react";

function LikeButton({ label }) {
  const [likes, setLikes] = useState(0);
  return (
    <button onClick={() => setLikes(likes + 1)}>
      {label}: {likes}
    </button>
  );
}

function App() {
  return (
    <ul>
      <li><LikeButton label="Post A" /></li>
      <li><LikeButton label="Post B" /></li>
    </ul>
  );
}

export default App;
result
Post A: 2
Post B: 1

Post A was clicked twice and Post B once, each keeping its own count. State is not shared unless you move it up to a common parent (the lesson on lifting state up). It also resets if the component is removed from the page and added again.

Different kinds of state

State can hold any JavaScript value: a number, string, boolean, array or object. Use one useState per independent piece of data:

jsx
import { useState } from "react";

function App() {
  const [open, setOpen] = useState(false);       // boolean
  const [name, setName] = useState("Asha");      // string
  const [likes, setLikes] = useState(0);         // number

  return (
    <div>
      <button className="toggle" onClick={() => setOpen(!open)}>
        {open ? "Hide" : "Show"} details
      </button>
      {open && (
        <p>
          {name} has {likes} likes.
        </p>
      )}
      <button className="like" onClick={() => setLikes(likes + 1)}>Like</button>
    </div>
  );
}

export default App;
result
Hide details
Asha has 1 likes.
Like

Here a boolean (open) controls what is shown, which combines with conditional rendering from the earlier lesson. The name state never changes in this example; in the next lessons you will see state for objects, arrays and form inputs.

State is a snapshot, not a live variable

After you call a setter, the variable in the current function does not change. The new value appears in the next render:

jsx
import { useState } from "react";

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

  function handleClick() {
    setCount(count + 1);
    console.log("right after setCount, count is still", count);
  }

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

export default App;
result
Count: 1
CONSOLE
right after setCount, count is still 0

The log prints 0 even though the button shows 1 afterwards. The next lesson explains this "snapshot" behaviour in depth, because it is the key to understanding React updates.

Initial state and expensive setup

The initial value is used only once, on the first render, but the expression you write is evaluated on every render, even though the result is ignored after the first. For a cheap value that does not matter. For an expensive calculation, pass a function (a lazy initializer), which React calls only the first time:

jsx
import { useState } from "react";

function buildInitialList() {
  console.log("building the list");
  return Array.from({ length: 3 }, (_, i) => "Item " + (i + 1));
}

function App() {
  const [items] = useState(buildInitialList);   // pass the function, do not call it
  return <p>{items.join(", ")}</p>;
}

export default App;
result
Item 1, Item 2, Item 3
CONSOLE
building the list
building the list

(StrictMode runs the initializer twice in development, so you may see the log twice.) useState(buildInitialList) passes the function; useState(buildInitialList()) would call it on every render. Lazy initialisation is useful for reading from localStorage or building a large structure.

The rules of hooks

Hooks are ordinary functions with two rules, which React's linter checks for you (react/rules-of-hooks in the Vite template):

  1. Call hooks only at the top level of a component or a custom hook. Never inside if, loops, nested functions, or after an early return.
  2. Call hooks only from React functions: components and custom hooks (functions whose name starts with use), not from ordinary utilities or event handlers.

The reason is that React does not store state by name. It stores it by the order of the hook calls in each component: the first useState is slot 1, the second is slot 2. If a condition made a call appear in one render and not in the next, the slots would shift and values would end up in the wrong variables:

jsx
function Bad({ loggedIn }) {
  if (loggedIn) {
    const [name, setName] = useState("");    // wrong: sometimes called, sometimes not
  }
  const [count, setCount] = useState(0);
  // ...
}

Always call the hook, and put the condition in how you use its value.

Do not change state directly

Assigning to the state variable (count = count + 1) does nothing useful, because React is not watching it. Always use the setter. For numbers and strings that is simple. For objects and arrays there is a further rule: replace, do not mutate. Mutating an array with push changes it behind React's back, and the screen may not update. The lesson on objects and arrays in state covers this.

Common mistakes

  • Using a plain variable (let count = 0) and expecting the screen to update.
  • Assigning directly: count = 5 instead of setCount(5).
  • Reading the state right after calling the setter and expecting the new value in the same function.
  • Calling useState inside a condition, a loop or an event handler.
  • Calling the initialiser instead of passing it: useState(compute()) versus useState(compute).
  • Naming the pair inconsistently ([a, setB]), which makes code hard to read.
  • Putting a value in state that can be calculated from other state or props. Calculate it during render instead (a later lesson).

Exercise

  1. Build a counter with three buttons: "-1", "+1" and "Reset" (sets the count to 0).
  2. Make a "Like" button that toggles between liked and not liked, using a boolean state, and shows a heart.
  3. Create a Toggle component that shows or hides a paragraph, and put three of them on the page. Do they share their state?
  4. Add a second state to the counter for a step size (1, 5 or 10), changed by buttons, so "+" adds the chosen step.

Key points

  • State is memory that belongs to one component instance and survives between renders; changing it makes React render the component again.
  • const [value, setValue] = useState(initial) gives the current value and a function to replace it; never assign to value directly.
  • A normal variable resets on every render and changing it does not update the screen, which is why state is needed.
  • Call hooks only at the top level of a component or custom hook, never inside conditions, loops or nested functions.

Check yourself

1. What does useState(0) return?
2. Why does changing a normal let variable inside a click handler not update the screen?
3. You render <Counter /> twice. How many separate counts exist?
4. Where can you call useState?