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] = x | arr.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.
