Lesson 5 of 12

React State with useState

Props let a parent give data to a child, but a component also needs its own memory: how many items are in the cart, which tab is open, what the user has typed. In React this memory is called state, and you add it with the React useState hook. State is what turns a static component into an interactive one.

Why a normal variable is not enough

Imagine a counter written with let count = 0 and a click handler that does count++. Nothing on the screen changes. Two things go wrong. First, changing a local variable does not tell React to render again. Second, even if React did render again, the function would run from the top and set count back to 0. State fixes both problems: React keeps the value safe between renders, and changing it schedules a new render.

Using React useState

Import useState from React and call it at the top of your component with the starting value. It returns an array with exactly two items, which you unpack with array destructuring: the current value and a setter function. By convention they are named something and setSomething.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>A step counter with useState</title>
  <script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
  <style>
    body { font-family: system-ui, sans-serif; padding: 16px; line-height: 1.5; }
    button { margin-right: 6px; padding: 4px 12px; }
  </style>
</head>
<body>
  <div id="root"></div>

  <script type="text/babel" data-type="module">
import React, { useState } from "https://esm.sh/[email protected]";
import { createRoot } from "https://esm.sh/[email protected]/client";

function StepCounter() {
  const [steps, setSteps] = useState(0);

  return (
    <div>
      <p>Steps walked on the beach: <strong>{steps}</strong></p>
      <button type="button" onClick={() => setSteps(steps + 100)}>+100</button>
      <button type="button" onClick={() => setSteps(0)}>Reset</button>
      {steps >= 1000 && <p>Great walk!</p>}
    </div>
  );
}

function App() {
  return (
    <>
      <h3>Priya</h3>
      <StepCounter />
      <h3>Arjun</h3>
      <StepCounter />
    </>
  );
}

createRoot(document.getElementById("root")).render(<App />);
  </script>
</body>
</html>

Click the buttons for Priya and Arjun separately. Each StepCounter has its own state, even though both come from the same function. State belongs to a particular place in the component tree, not to the function itself.

When you call setSteps(100), React stores the new value and calls your component again. This time useState returns 100, the JSX is calculated with the new number, and React updates the page.

The rules of hooks

Functions whose names start with use are called hooks. React relies on hooks being called in the same order on every render, so there are two rules:

  • Call hooks only at the top level of a component – not inside conditions, loops or nested functions.
  • Call hooks only from React components or from your own custom hooks, never from ordinary JavaScript functions.

State is a snapshot

Calling the setter does not change the variable straight away. Inside one render, steps is a fixed value – a snapshot. So if a click handler calls setSteps(steps + 1) three times, all three calls read the same old steps, and the count goes up by one, not three.

When the new value depends on the previous one, pass an updater function instead: setSteps(s => s + 1). React queues these functions and runs each one with the latest value, so three calls really add three. Try both buttons below.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Plain value versus updater function</title>
  <script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
  <style>
    body { font-family: system-ui, sans-serif; padding: 16px; line-height: 1.5; }
    button { margin: 4px 6px 4px 0; padding: 4px 12px; }
  </style>
</head>
<body>
  <div id="root"></div>

  <script type="text/babel" data-type="module">
import React, { useState } from "https://esm.sh/[email protected]";
import { createRoot } from "https://esm.sh/[email protected]/client";

function App() {
  const [score, setScore] = useState(0);

  function addThreeWrong() {
    setScore(score + 1);
    setScore(score + 1);
    setScore(score + 1);
  }

  function addThreeRight() {
    setScore(s => s + 1);
    setScore(s => s + 1);
    setScore(s => s + 1);
  }

  return (
    <div>
      <p>Cricket score: <strong>{score}</strong></p>
      <button type="button" onClick={addThreeWrong}>+3 with values (adds 1)</button>
      <button type="button" onClick={addThreeRight}>+3 with updaters (adds 3)</button>
      <button type="button" onClick={() => setScore(0)}>Reset</button>
    </div>
  );
}

createRoot(document.getElementById("root")).render(<App />);
  </script>
</body>
</html>

Objects and arrays in state

State can hold any value, including objects and arrays. The important rule is: never change them in place. Writing user.city = "Salem" or items.push(x) changes the existing object, React sees the same object as before, and the screen may not update. Instead, create a new object or array and pass it to the setter. The spread syntax ... makes this easy.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Updating an object and an array in state</title>
  <script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
  <style>
    body { font-family: system-ui, sans-serif; padding: 16px; line-height: 1.5; }
    button { margin: 4px 6px 4px 0; padding: 4px 12px; }
  </style>
</head>
<body>
  <div id="root"></div>

  <script type="text/babel" data-type="module">
import React, { useState } from "https://esm.sh/[email protected]";
import { createRoot } from "https://esm.sh/[email protected]/client";

function App() {
  const [user, setUser] = useState({ name: "Meena", city: "Thanjavur" });
  const [visits, setVisits] = useState(["Thanjavur"]);

  function moveTo(city) {
    setUser({ ...user, city: city });   // copy, then change one key
    setVisits([...visits, city]);       // new array with one more item
  }

  return (
    <div>
      <p>{user.name} lives in <strong>{user.city}</strong>.</p>
      <button type="button" onClick={() => moveTo("Madurai")}>Move to Madurai</button>
      <button type="button" onClick={() => moveTo("Tiruchirappalli")}>Move to Tiruchirappalli</button>
      <p>Cities so far: {visits.join(" → ")}</p>
    </div>
  );
}

createRoot(document.getElementById("root")).render(<App />);
  </script>
</body>
</html>
Instead of (mutating)Write (new copy)
arr.push(x)[...arr, x]
arr.splice(i, 1)arr.filter(item => item.id !== id)
arr[i] = xarr.map((item, j) => j === i ? x : item)
obj.key = value{ ...obj, key: value }

Where should state live?

Put state in the component that uses it. If two sibling components need the same value – for example, a filter panel and a results list – move the state up to their closest common parent and pass it down as props, along with a function to change it. React calls this lifting state up. It keeps one source of truth, so the two siblings can never disagree.

Tip: Keep state minimal. If a value can be calculated from existing state or props – a total price from a list of items, or a full name from first and last name – calculate it during rendering instead of storing it in a second piece of state. Duplicate state easily gets out of sync.

Further reading

Key takeaways

  • State is a component's memory; add it with const [value, setValue] = useState(initial).
  • Calling the setter stores the new value and makes React render the component again.
  • Each component instance has its own separate state.
  • Call hooks only at the top level of components or custom hooks.
  • Use an updater function setX(prev => …) when the new value depends on the old one.
  • Never mutate objects or arrays in state; create new copies with spread, map or filter.

Quick quiz

Check what you learned – pick an answer.

1. What does useState(0) return?

2. count is 5. A click handler runs setCount(count + 1) twice. What is count after the re-render?

3. Which is the correct way to add "Ooty" to an array called places in state?

Practice exercise

Build a LikeButton component. It keeps a liked state (true/false) and a count state starting at 12. Clicking the button toggles liked, adds 1 when liking and subtracts 1 when unliking, and changes the label between "Like" and "Liked".

Show a hint

Read the current liked value inside the click handler to decide whether to add or subtract, then flip it with setLiked(!liked).

Show the solution
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Like button</title>
  <script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
  <style>
    body { font-family: system-ui, sans-serif; padding: 16px; line-height: 1.5; }
    .liked { background: #ffe3e8; border-color: #d6336c; }
  </style>
</head>
<body>
  <div id="root"></div>

  <script type="text/babel" data-type="module">
import React, { useState } from "https://esm.sh/[email protected]";
import { createRoot } from "https://esm.sh/[email protected]/client";

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

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

  return (
    <button type="button" className={liked ? "liked" : ""} onClick={handleClick}>
      {liked ? "Liked" : "Like"} · {count}
    </button>
  );
}

function App() {
  return <LikeButton />;
}

createRoot(document.getElementById("root")).render(<App />);
  </script>
</body>
</html>

Progress is saved in this browser only.