State with useState
Props come from outside a component. But many components also need to remember things of their own: how many items are in the cart, which tab is open, what the user has typed. This component memory is called state, and the useState hook gives it to you.
In this lesson you will see why an ordinary variable is not enough, use useState, understand that state behaves like a snapshot, use updater functions for several updates in a row, and learn the rules every hook follows.
Why a normal variable does not work
Here is a counter that tries to use a plain variable. Click the first button a few times:
import { useState } from "react";
import { createRoot } from "react-dom/client";
function BrokenCounter() {
let plates = 0;
return (
<p>
Plain variable: {plates}{" "}
<button
onClick={() => {
plates = plates + 1;
console.log("plates is now", plates);
}}
>
Serve a plate
</button>
</p>
);
}
function WorkingCounter() {
const [plates, setPlates] = useState(0);
return (
<p>
State: {plates} <button onClick={() => setPlates(plates + 1)}>Serve a plate</button>
</p>
);
}
createRoot(document.getElementById("root")).render(
<>
<BrokenCounter />
<WorkingCounter />
</>
);Clicking the first button three times prints the following, but the screen still shows 0:
plates is now 1 plates is now 2 plates is now 3
Two things go wrong. First, changing a local variable does not tell React to render again, so the screen never updates. Second, even if something else caused a render, the function would run from the top and let plates = 0 would reset it. State fixes both: React stores the value outside your function between renders, and changing it with the setter schedules a new render.
Using useState
import { useState } from "react";
const [plates, setPlates] = useState(0);useState(0)declares a state variable with the initial value0. The initial value is used only on the first render.- It returns an array with exactly two items, which you unpack with array destructuring: the current value and a setter function.
- Name them as a pair:
[thing, setThing], such as[city, setCity]or[isOpen, setIsOpen].
Calling setPlates(5) does two things: it stores 5 as the new value and asks React to render the component again. On that next render, useState(0) ignores the 0 and returns 5.
State can hold any value: a number, string, boolean, array or object. A component can have as many state variables as it needs, and each instance of a component has its own separate state. If you render <WorkingCounter /> twice, each one counts on its own.
State is a snapshot
This surprises almost everyone. Inside one render, a state variable is a fixed value. Calling the setter does not change the variable you already have; it only affects the next render. Try the "+3" button below:
import { useState } from "react";
import { createRoot } from "react-dom/client";
function RunsCounter() {
const [runs, setRuns] = useState(0);
function addThreeWrong() {
setRuns(runs + 1);
setRuns(runs + 1);
setRuns(runs + 1);
console.log("Inside the handler, runs is still", runs);
}
function addThreeRight() {
setRuns((r) => r + 1);
setRuns((r) => r + 1);
setRuns((r) => r + 1);
}
return (
<div>
<h3>Runs: {runs}</h3>
<button onClick={addThreeWrong}>+3 (wrong)</button>{" "}
<button onClick={addThreeRight}>+3 (right)</button>
</div>
);
}
createRoot(document.getElementById("root")).render(<RunsCounter />);Clicking "+3 (wrong)" once moves the runs from 0 to only 1, and prints:
Inside the handler, runs is still 0
In that render runs is 0, so all three calls say "set runs to 0 + 1". The result is 1, not 3. Also, the console.log after the setters still sees 0, because the variable in this render never changes.
Updater functions and batching
When the new state depends on the old state, pass a function to the setter: setRuns((r) => r + 1). This is an updater function. React puts the updaters in a queue and, during the next render, runs them one after another, each receiving the result of the previous one. So "+3 (right)" really adds three.
React also batches updates: all setter calls inside one event handler are collected and processed together, causing a single re-render instead of three. This is why your handler can call several setters without making the page flicker.
A simple rule: if the next value is calculated from the previous one, use an updater. If you are setting a completely new value, such as setCity("Pune"), a plain value is fine.
The rules of hooks
useState is a hook: a special function whose name starts with use. Hooks let function components use React features. They follow two rules:
- Call hooks only at the top level of a component, not inside
ifstatements, loops or nested functions. React identifies each state variable by the order of hook calls, so that order must be the same on every render. - Call hooks only from React components or from your own custom hooks, never from ordinary JavaScript functions.
function Profile({ isLoggedIn }) {
if (isLoggedIn) {
const [name, setName] = useState(""); // wrong: hook inside a condition
}
const [theme, setTheme] = useState("light"); // right: top level, always called
}The project template's linter (the rules-of-hooks check) warns you when you break these rules.
Advanced: initial values and resetting state
If the initial value is expensive to compute, such as reading and parsing saved data, pass a function instead of a value. React calls it only on the first render:
const [notes, setNotes] = useState(() => JSON.parse(localStorage.getItem("notes") ?? "[]"));Writing useState(JSON.parse(...)) would parse the data on every render and then throw the result away.
State belongs to a component's position in the tree. React keeps it as long as the same component type stays at the same place. To deliberately start fresh, give the component a different key. Here, switching the batter resets the counter:
import { useState } from "react";
import { createRoot } from "react-dom/client";
function BatterScore({ name }) {
const [balls, setBalls] = useState(0);
return (
<p>
{name} has faced {balls} balls <button onClick={() => setBalls(balls + 1)}>+1 ball</button>
</p>
);
}
function Innings() {
const [batter, setBatter] = useState("Aarav");
return (
<>
<button onClick={() => setBatter(batter === "Aarav" ? "Meera" : "Aarav")}>Change batter</button>
<BatterScore key={batter} name={batter} />
</>
);
}
createRoot(document.getElementById("root")).render(<Innings />);Without key={batter}, Meera would inherit Aarav's ball count, because React would see the same BatterScore at the same position and keep its state.
Common mistakes
- Changing state directly.
runs = 5orruns++does nothing useful. Always call the setter. - Reading state right after setting it. The variable still holds the old snapshot. Use the value you just calculated instead.
- Several updates based on stale state. Use updater functions:
setX((x) => x + 1). - Storing values you can calculate. If
totalcan be computed fromitems, compute it during render instead of keeping a second state variable that can go out of sync. - Hooks in conditions or loops. Keep every hook call at the top level of the component.
Key points
- State is component memory that survives re-renders;
useState(initial)returns[value, setValue]. - Calling the setter stores the new value and triggers a re-render; plain variables do neither.
- State is a snapshot: within one render the variable never changes.
- Use an updater function,
setX((prev) => ...), when the next value depends on the previous one. - React batches all updates from one event into a single re-render.
- Call hooks only at the top level of components or custom hooks; change a
keyto reset state.
Exercise
Build a ChaiStall component with two state variables: cups (starting at 0) and customer (starting as "Walk-in"). Add buttons "+1 cup", "+5 cups" (using updater functions so it really adds 5) and "Reset". Add a text input that updates customer as the user types. Show "Customer: NAME, cups: N, bill: ₹X" at ₹15 per cup, with the bill calculated during render.
Show solution
import { useState } from "react";
import { createRoot } from "react-dom/client";
function ChaiStall() {
const [cups, setCups] = useState(0);
const [customer, setCustomer] = useState("Walk-in");
function addFive() {
for (let i = 0; i < 5; i++) {
setCups((c) => c + 1);
}
}
return (
<div>
<input value={customer} onChange={(e) => setCustomer(e.target.value)} />
<p>Customer: {customer}, cups: {cups}, bill: ₹{cups * 15}</p>
<button onClick={() => setCups((c) => c + 1)}>+1 cup</button>{" "}
<button onClick={addFive}>+5 cups</button>{" "}
<button onClick={() => setCups(0)}>Reset</button>
</div>
);
}
createRoot(document.getElementById("root")).render(<ChaiStall />);Five updater calls queue five increments, so the count really rises by five. The bill is not stored in state, because it can always be calculated from cups.
Quick Quiz
1. What does useState(0) return?
useState returns a pair, usually unpacked as const [value, setValue] = useState(0).
2. count is 4. A handler calls setCount(count + 1) three times. What is count after the re-render?
Each call uses the snapshot value 4, so all three set the count to 5.
3. Which call correctly adds 1 based on the latest queued value?
An updater function receives the most recent pending value, so several of them add up correctly.
4. Why must hooks not be called inside an if statement?
Skipping a hook call in one render would shift every later hook to the wrong state.
Learn React with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
