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:
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;Clicked 0 times
CONSOLE
count is now 1
count is now 2
count is now 3The Console shows count changing, but the button text always stays at 0. There are two reasons, and both matter:
- A local variable does not survive. Each time React renders the component, the function runs from the top, and
let count = 0creates a fresh variable. Whatever you changed last time is gone. - 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:
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;Clicked 3 timesNow 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. The0is 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:
countis the current value, andsetCountis 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 newcount.
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:
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;Post A: 2
Post B: 1Post 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:
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;Hide details
Asha has 1 likes.
LikeHere 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:
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;Count: 1
CONSOLE
right after setCount, count is still 0The 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:
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;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):
- Call hooks only at the top level of a component or a custom hook. Never inside
if, loops, nested functions, or after an earlyreturn. - 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:
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 = 5instead ofsetCount(5). - Reading the state right after calling the setter and expecting the new value in the same function.
- Calling
useStateinside a condition, a loop or an event handler. - Calling the initialiser instead of passing it:
useState(compute())versususeState(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
- Build a counter with three buttons: "-1", "+1" and "Reset" (sets the count to 0).
- Make a "Like" button that toggles between liked and not liked, using a boolean state, and shows a heart.
- Create a
Togglecomponent that shows or hides a paragraph, and put three of them on the page. Do they share their state? - Add a second state to the counter for a
stepsize (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.