So far our components show fixed data. Real pages change: a counter goes up, a menu opens, a message appears. To remember data that changes, a component needs state. In this lesson you will learn state with the useState hook, step by step, using small examples.
Why a normal variable does not work
Let us try to build a counter with a plain variable. It looks right, but it does not work:
function Counter() {
let count = 0;
function add() {
count = count + 1;
console.log(count);
}
return <button onClick={add}>Clicked {count} times</button>;
}
If you click, the console shows 1, 2, 3, but the button always says 0. There are two reasons. First, React does not know that count changed, so it does not draw the component again. Second, even if it did, a function component starts from the top each time it is drawn, so let count = 0 would run again and reset the number. We need a place where React can keep the value safe between draws, and where changing it tells React to draw again. That place is state.
The useState hook
A hook is a special function whose name starts with use. Hooks let a component use React features. useState gives a component a piece of state.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}
Let us read the key line, const [count, setCount] = useState(0);:
useState(0)creates a piece of state and says its first value is 0.- It returns an array with two things. The first is the current value, which we call
count. The second is a function to change it, which we callsetCount. - We use array destructuring to give them names. You can choose any names, but the pattern
[thing, setThing]is the usual way.
When you click, setCount(count + 1) tells React: “the new value is one more”. React stores it, draws the component again, and this time count is 1. The button shows “Clicked 1 times”. The same happens for each click.
What happens when state changes
- You call the setter function, for example
setCount(5). - React saves the new value.
- React calls your component function again (a re-render).
- This time
useStategives back the new value. - React compares the new result with the old one and updates the page where it is different.
The big idea: you never change the page yourself. You change the state, and the page follows.
State can be any type
State is not only for numbers. Here are a few common kinds:
const [name, setName] = useState(''); // text
const [isOpen, setIsOpen] = useState(false); // true or false
const [items, setItems] = useState([]); // a list
const [user, setUser] = useState(null); // an object, or nothing yet
const [form, setForm] = useState({ email: '', age: 0 }); // an object
Example: a toggle
function Menu() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>
{isOpen ? 'Close menu' : 'Open menu'}
</button>
{isOpen && (
<ul>
<li>Home</li>
<li>About</li>
</ul>
)}
</div>
);
}
Each click flips isOpen between true and false. The text on the button and the list both depend on it. You wrote no code to show or hide anything by hand. You only described what to show for each value.
Example: a text box
function Hello() {
const [name, setName] = useState('');
return (
<div>
<input
value={name}
onChange={(event) => setName(event.target.value)}
placeholder="Your name"
/>
<p>Hello, {name || 'stranger'}!</p>
</div>
);
}
Every key press makes a change event. The handler reads the new text with event.target.value and puts it in state. The input shows what is in state through its value. This pattern is called a controlled input, and the forms lesson explains it fully.
The golden rule: never change state directly
State is read-only. You must always use the setter. If you write count = count + 1 or items.push('x'), React does not notice, and the screen does not change. For numbers and text this is easy. For arrays and objects, you must make a new copy and pass the copy to the setter.
// Add to an array: make a new array
setItems([...items, 'new item']);
// Remove from an array
setItems(items.filter((item) => item !== 'old item'));
// Change an object: copy it, then replace one field
setUser({ ...user, name: 'Asha' });
// Change one item in a list
setItems(items.map((item) => item.id === 3 ? { ...item, done: true } : item));
The three dots ... are called the spread syntax. They copy all the items of an array, or all the fields of an object, into a new one. React decides whether something changed by checking if the value is a new object, so a copy is required. This idea is called immutability, and it is one of the most important habits to learn.
State updates are not instant
Calling the setter does not change the variable in the code that is running right now. It asks React to draw again with the new value. This surprises many beginners:
function Demo() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
console.log(count); // still the OLD value
}
return <button onClick={handleClick}>{count}</button>;
}
The console shows the old number, because that variable belongs to the draw that is already finished. The new value arrives in the next draw.
There is a second problem. If you call setCount(count + 1) three times in a row, you get +1, not +3, because each call used the same old count. When the new value depends on the old value, pass a function. React gives it the latest value:
setCount((current) => current + 1);
setCount((current) => current + 1);
setCount((current) => current + 1); // now it really adds 3
Where should state live?
- Each
useStatecall belongs to one component. Two copies ofCounterhave two separate counts. - Keep state as small as you can. Do not store data you can work out from other data. For example, store the list, and calculate its length when you need it.
- If two components need the same state, move it to their closest shared parent. Lesson 14 covers this.
Also remember the rules of hooks: call them only at the top level of a component, never inside an if, a loop, or a nested function. And call them only inside components or custom hooks. React depends on the order of the hook calls staying the same on every draw.
Common mistakes
- Changing state directly (
count++,items.push(...)). - Forgetting to copy an object or array before changing it.
- Expecting the variable to change right after calling the setter.
- Calling hooks inside conditions or loops.
- Storing something you could calculate, which makes two values that can get out of step.
One more example: a like button
To finish, here is a small component that joins three ideas from this lesson: state, an event and a conditional message.
function LikeButton() {
const [liked, setLiked] = useState(false);
const [count, setCount] = useState(0);
function handleClick() {
setLiked(!liked);
setCount((c) => (liked ? c - 1 : c + 1));
}
return (
<button onClick={handleClick} aria-pressed={liked}>
{liked ? 'Liked' : 'Like'} ({count})
</button>
);
}
There are two pieces of state, and both change in one click. React waits until the handler finishes, and then draws once. The text on the button comes from state, and aria-pressed tells screen readers whether the button is on or off. Try to find the other places in your own projects where a small piece of state like this would help.
Your task
- Build a counter with three buttons: add one, subtract one, and reset to zero.
- Build a to-do list with an input and an Add button. Keep the list in state and add items using the spread syntax.
- Add a Remove button to each item using
filter.
How to check it worked
- The counter never goes wrong when you click quickly.
- New to-do items appear at once and the input still works.
- Removing an item removes only that item.
Summary
- State is data that changes.
useState(initial)returns the value and a setter. - A call to the setter makes React draw the component again with the new value.
- Never change state directly. Make a new array or object with spread,
maporfilter. - If the new value depends on the old one, use
setX((old) => ...).