State and useState
State as React's unit of re-render. The exact model of useState, functional updates, and patterns for updating object state.
In Chapter 4 we covered components and props. But every component we built was static. Once rendered, the appearance never changed. Real apps need the screen to update when the user clicks a button, types something, or when data arrives. In this chapter we learn state — the way a component handles data that can change.
useState is the tool you will reach for most often in this whole book. Chapter 18 (typing hooks) refines it again with TypeScript, and Chapter 24 (Server vs Client Components) revisits the boundary of where useState can and cannot be used.
Why won’t a regular variable work? #
The first thing that comes to mind is, “Can’t we just change a variable’s value?” Let us try.
function App() {
let count = 0;
function handleClick() {
count = count + 1;
console.log('count:', count);
}
return (
<div>
<p>Current count: {count}</p>
<button onClick={handleClick}>+1</button>
</div>
);
}
export default App;Click the button and the console nicely shows count: 1, count: 2, count: 3. But the number on screen stays at 0. Why?
When React draws the screen, it runs the component function once and reflects the result on screen. Changing a regular variable’s value never signals to React that “we need to draw again,” so the screen does not update. On top of that, every time the component function gets called again, let count = 0 runs from scratch, so the changed value does not survive to the next render either.
To make React re-draw the screen while keeping the value across renders, you need a special store called state.
The useState hook #
You create state by calling a function named useState. This function is one of the Hooks React provides.
use (useState, useEffect, useContext, and so on). The deeper story about hooks is covered in Chapter 13 (Custom hooks); for now, “a special function React provides” is enough.Let us rebuild the counter using useState.
import { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<div>
<p>Current count: {count}</p>
<button onClick={handleClick}>+1</button>
</div>
);
}
export default App;This time, when you click the button, the number on screen really does increase.
A look inside useState #
Let us go through the code above line by line.
import { useState } from 'react';We import useState from the react package. You always import a hook to use it.
const [count, setCount] = useState(0);Calling useState(0) returns an array of length 2. We receive both values at once via JavaScript destructuring.
- The first,
count— the current state value. Initially it is the value you passed touseState(0). - The second,
setCount— the function to change state. You must call this function for React to re-draw the screen.
You can name them anything, but by convention you use the pattern [value, setValue]: [name, setName], [isOpen, setIsOpen], and so on.
setCount(count + 1);When you call setCount with a new value, React (1) updates the state and (2) re-renders the component. On re-render, the component function runs from the start again, and this time useState(0) hands back the newly updated value (1).
What happens when state changes #
If you keep this picture in your head, React code reads much more clearly going forward.
- User clicks a button
handleClickruns →setCount(1)is called- React updates the state to
1and re-renders the component - The
Appfunction runs from the start again - This time
useState(0)returns[1, setCount] - New JSX (
<p>Current count: 1</p>) is built - React diffs against the previous screen and applies only the changed bits to the real DOM
The important point: every time state changes, the entire component function runs again. That is why a regular variable like let count = 0 declared inside the component gets re-initialized each time and does not retain its value. State is stored somewhere inside React, separate from the function, and survives across renders.
Various types of state #
State can hold any JavaScript value, not just numbers.
const [name, setName] = useState(''); // string
const [isOpen, setIsOpen] = useState(false); // boolean
const [items, setItems] = useState([]); // array
const [user, setUser] = useState({ name: '', age: 0 }); // object
const [selected, setSelected] = useState(null); // nullAs you will see in Chapter 18 (typing hooks), TypeScript infers the type from the initial value. useState('') infers string; useState(0) infers number.
Do not modify state directly #
The most common mistake. The following code does not work.
const [count, setCount] = useState(0);
function handleClick() {
count = count + 1; // 🚫 direct modification
}const [items, setItems] = useState(['apple']);
function addItem() {
items.push('banana'); // 🚫 mutating the array directly
setItems(items);
}You must change state only by passing a new value through the set function. For arrays and objects, the rule is to build a new array / new object and pass it.
const [items, setItems] = useState(['apple']);
function addItem() {
setItems([...items, 'banana']); // build a new array and pass it
}const [user, setUser] = useState({ name: 'Cheolsu', age: 30 });
function birthday() {
setUser({ ...user, age: user.age + 1 }); // build a new object and pass it
}The most common pattern is to spread the existing value with ... and then overwrite the parts you want to change.
items.push(...) only changes the contents of the same array — the reference stays the same — so React thinks nothing has changed and does not re-render. You must hand it a new array / object so React goes, “huh, this is different,” and updates the screen.Functional updates #
When you want to update state based on the previous value, you can pass a function to the set function.
function handleClick() {
setCount(prev => prev + 1);
}It is almost the same as setCount(count + 1), but because it receives the previous value safely, it works correctly when you need to call the setter several times in a row.
function handleClick() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}This code looks like it should increase count by 3 on a single click, but in fact it only goes up by 1. All three calls see the same count value and try to compute count + 1.
function handleClick() {
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
}Written this way, each call receives the result of the previous one, so count goes up by exactly 3. Most of the time setCount(count + 1) is fine too, but functional updates that explicitly say “update based on the previous value” are the safer default. This book prefers functional updates whenever feasible.
Multiple pieces of state #
You can call useState as many times as you like inside one component.
import { useState } from 'react';
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
// ... input handling logic ...
}Values of different nature are usually kept in separate states. You can lump them into one object too, but every update means spreading it back out, which makes the code longer, so simple values are easier as separates. Chapter 9 (Forms) compares both side by side.
Try it yourself #
Let us build a counter component with +1, -1, and Reset buttons.
Create a new src/Counter.jsx.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div style={{ padding: '16px', border: '1px solid #ccc', borderRadius: '8px' }}>
<h2>Count: {count}</h2>
<button onClick={() => setCount(prev => prev + 1)}>+1</button>
<button onClick={() => setCount(prev => prev - 1)}>-1</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
export default Counter;Use it from src/App.jsx.
import Counter from './Counter';
function App() {
return (
<>
<h1>Counter demo</h1>
<Counter />
<Counter />
</>
);
}
export default App;There is something interesting. We used <Counter /> twice, but each counter has its own count and works independently. Pressing +1 on one does not affect the number on the other. State is kept separately per component instance.
Exercises #
- Add a new button
+10toCounterso that one press adds 10 at once. Use the functional update pattern (setCount(prev => prev + 10)). - Add
minandmaxprops toCounter, and prevent the count from going belowminor abovemax. With<Counter min={0} max={10} />, going under 0 or over 10 should not be possible. Handle it withMath.max/Math.mininside the functional update. - Practice with object state. Build a
Usercomponent withuseState({ name: 'Cheolsu', age: 30 })as the initial value. A “Change name” button usesprev => ({ ...prev, name: 'Younghee' }), and a “+1 to age” button usesprev => ({ ...prev, age: prev.age + 1 }). Repeat until the pattern of spreading into a new object feels natural in your hands.
In one line: A regular variable cannot update the screen. Use
useStateto get[value, setValue]and change it only through the set function. For arrays and objects, build a new value ([...arr, x],{ ...obj, k: v }). When updating based on the previous value, functional updates (setX(prev => ...)) are the safe default.
Next chapter #
We have been using event handlers like onClick just by reaching for them, without looking closely. In the next chapter, Chapter 6: Event handling, we take a proper look at how React handles events, how to pull information out of the event object, and we lay the bridge over to Chapter 19 (Typing events and forms).