State
In short: State is a value a component remembers between renders; in React it is kept with useState, and every change renders the component again.
Problem
The button does not count
The product card has an "Add" button and an "In the cart" counter. Press "Add" three times.
Console
The console says count = 1, 2, 3. The card says 0. There is no error. The variable count changes, but the screen does not follow it. Where is the difference?
The idea
A plain variable is not enough
The card is not drawn by your code but by the library. It draws again only when it knows that something changed. count++ changes the variable, but tells nobody.
ProductCard is a function. React calls it when it draws the card, and it returns what to show. let count = 0 is a local variable of that call. The click changes it, but nobody calls the function again, so the card stays at 0.
There is a second problem. If React calls the function again for another reason, the first line is let count = 0 again. Everything collected in the variable is lost.
You need data that the component remembers between two renders and whose change draws it again. This is called state.
The idea
useState and ref
const [count, setCount] = useState(0);
function add() { setCount(count + 1);}useState(0)returns two things: the value and the function that changes it. The zero is only the initial value.countis the value for this render.setCount(1)remembers the new value and tells React to draw the card again. React callsProductCardagain, and this timeuseStatereturns 1.
useState is a hook: a React function that is called at the top of the component, never inside an if or a loop. React tells the states apart by the order in which they are called.
The click handler is given as a function: onClick={add}. If you write onClick={add()}, add is called while the card is being drawn. It changes the state, which draws again, and React stops with the error "Too many re-renders".
Console
See it
See what happens on a click
function ProductCard({ product }) { const [count, setCount] = useState(0);
function addThree() { setCount(count + 1); setCount(count + 1); setCount(count + 1); }
return ( <article> <button onClick={addThree}>+3</button> <p>In the cart: {count}</p> </article> );}Step by step, in words
- The card keeps count in React’s state: useState(0) gives the value and the function that changes it.
- React calls the function ProductCard. It does not touch the page: it returns a description, a button and a text with count = 0.
- The first time, the description becomes real elements in the DOM. The page now shows “In the cart: 0”.
- A click on +3: the browser calls addThree.
- setCount(count + 1) three times. In this call count is a snapshot: 0. Each time “become 1” goes into the queue. The screen does not change yet.
- The handler is done. React applies the queue: 1, 1, 1 gives 1. Then it calls ProductCard again, and now count is 1.
- React compares the new description with the old one. The button is the same, only the text changed. In the DOM only the text changes.
- That is why the counter shows 1, not 3. When the new value depends on the old one, pass a function: setCount((c) => c + 1).
The card is not built again. The library compares the new description with the old one and changes only the difference in the DOM: the text. The button is the same element and the focus stays on it. That is the difference from render() in the DOM module, which replaced everything.
Try it
Fix the counter
This is the card from the start. Make count state, so that after every click on "Add" the card shows the new number.
Console
Solution
ProductCard.jsximport { useState } from 'react';
const money = (x) => '€' + x.toFixed(2);
export default function ProductCard({ product }) {
const [count, setCount] = useState(0);
function add() {
setCount(count + 1);
}
return (
<article className="card">
<h2>{product.name}</h2>
<p>{money(product.price)}</p>
<button type="button" onClick={add} aria-label={`Add ${product.name}`}>
Add
</button>
<p>In the cart: {count}</p>
</article>
);
}
App.jsximport ProductCard from './ProductCard';
const product = { id: 1, name: 'White bread', price: 1.2 };
export default function App() {
return (
<main>
<h1>The shop</h1>
<ProductCard product={product} />
</main>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 8px 16px; }
h1 { font-size: 22px; }
.card { border: 1px solid #ddd5c4; border-radius: 8px; padding: 4px 16px 12px; margin-bottom: 12px; max-width: 340px; }
.card h2 { font-size: 18px; margin: 8px 0 4px; }
.card p { margin: 6px 0; }
button { font: inherit; padding: 4px 12px; margin-right: 6px; }
let count = 0 becomes const [count, setCount] = useState(0), and count++ becomes setCount(count + 1). useState has to be imported: import { useState } from 'react'. If count++ stays, the value changes, but React does not draw again.The idea
The value is a snapshot
Mineral water is also sold in a pack of 6 bottles. It is handy for the pack button to call add() six times. Here the two libraries behave differently.
count is a constant of one render. setCount does not change count in the handler that is running now. It puts a request in a queue, and React applies it after the handler:
function addBox() { for (let i = 0; i < 6; i++) { setCount(count + 1); // 0 + 1, six times: "become 1" }}The six requests are "become 1" and the counter becomes 1, not 6. Say "add 1 to the latest value": pass a function instead of a value.
setCount((c) => c + 1);React calls each such function with the result of the previous one: 0 → 1 → 2 … → 6. The rule: when the new value depends on the old one, pass a function.
Try it
Find the bug
"Add a pack" should add 6 bottles. Which line is to blame?
Pick the line with the bug.
Line 5: count is 0 in each of the six calls, so the requests are "become 1". It should be setCount((c) => c + 1) or straight setCount((c) => c + 6).
Try it
A pack of water
Write addBox() so that "Add a pack (6 pcs)" adds 6 bottles to what is already in the cart. The check presses "Add" once, then the pack twice, and expects 1, 7 and 13.
Console
Solution
ProductCard.jsximport { useState } from 'react';
const money = (x) => '€' + x.toFixed(2);
export default function ProductCard({ product }) {
const [count, setCount] = useState(0);
function add() {
setCount((c) => c + 1);
}
function addBox() {
setCount((c) => c + 6);
}
return (
<article className="card">
<h2>{product.name}</h2>
<p>{money(product.price)}</p>
<button type="button" onClick={add} aria-label={`Add ${product.name}`}>
Add
</button>
<button type="button" onClick={addBox}>Add a pack (6 pcs)</button>
<p>In the cart: {count}</p>
</article>
);
}
App.jsximport ProductCard from './ProductCard';
const product = { id: 15, name: 'Mineral water', price: 0.9 };
export default function App() {
return (
<main>
<h1>The shop</h1>
<ProductCard product={product} />
</main>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 8px 16px; }
h1 { font-size: 22px; }
.card { border: 1px solid #ddd5c4; border-radius: 8px; padding: 4px 16px 12px; margin-bottom: 12px; max-width: 340px; }
.card h2 { font-size: 18px; margin: 8px 0 4px; }
.card p { margin: 6px 0; }
button { font: inherit; padding: 4px 12px; margin-right: 6px; }
setCount((c) => c + 6) adds 6 to the latest value. Six times setCount(count + 1) gives 2: count is 1 in each of them. setCount(6) does not add, it sets 6, and after "Add" the counter is 6, not 7. In the solution add uses a function too: that way neither button depends on the snapshot.The idea
Each card remembers its own
State belongs to the instance, not to the code. The catalogue has two cards from the same ProductCard, and each has its own count:
Console
Press "Add" on the bread twice and on the milk once: 2 and 1. The library keeps the state of each card by its place in the tree of components. Remove the card from the page and its state goes with it. When two cards must show the same number, it has to live in their common parent. That is the lesson on shared state in this module.
More precisely, if you are curious
Why a snapshot. React does not change count in place, because the function was called with this value, and everything in it, the handlers too, sees that value. So one render is consistent: whatever is computed from count is computed from one and the same value.
When React does not draw. If setCount gets the same value (compared with Object.is), React skips the render. This matters in the next lesson, for arrays.
Updates are batched. All setCount calls in one handler give one render, and since React 18 also in setTimeout and after await.
StrictMode. In development <StrictMode> calls every component twice, to catch a component that changes something outside while it draws. The sandbox does not use it, and a Vite project has it in main.jsx.
Class components. Older React code writes the component as a class with this.state and this.setState. New code uses functions and hooks.
Exercise
Choose the quantity
The catalogue has three cards. In each one add a quantity that starts at 1, a "−" button (name for the screen reader: "Fewer White bread"), a "+" button ("More White bread") and a line "2 × €1.20 = €2.40" with the quantity, the price and the total. "−" is disabled when the quantity is 1. Each card counts on its own.
Console
Solution
ProductCard.jsximport { useState } from 'react';
const money = (x) => '€' + x.toFixed(2);
export default function ProductCard({ product }) {
const [count, setCount] = useState(1);
function less() {
setCount(count - 1);
}
function more() {
setCount(count + 1);
}
return (
<article className="card">
<h2>{product.name}</h2>
<p>{money(product.price)}</p>
<p>
<button type="button" onClick={less} disabled={count === 1} aria-label={`Fewer ${product.name}`}>−</button>
<button type="button" onClick={more} aria-label={`More ${product.name}`}>+</button>
</p>
<p>{count} × {money(product.price)} = {money(count * product.price)}</p>
</article>
);
}
App.jsximport ProductCard from './ProductCard';
const products = [
{ id: 1, name: 'White bread', price: 1.2 },
{ id: 5, name: 'Fresh milk', price: 1.6 },
{ id: 7, name: 'White cheese', price: 7.9 },
];
export default function App() {
return (
<main>
<h1>The shop</h1>
{products.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</main>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 8px 16px; }
h1 { font-size: 22px; }
.card { border: 1px solid #ddd5c4; border-radius: 8px; padding: 4px 16px 12px; margin-bottom: 12px; max-width: 340px; }
.card h2 { font-size: 18px; margin: 8px 0 4px; }
.card p { margin: 6px 0; }
button { font: inherit; padding: 4px 12px; margin-right: 6px; }
ProductCard: const [count, setCount] = useState(1), less calls setCount(count - 1), and more calls setCount(count + 1). "−" is disabled={count === 1}: that way the quantity cannot drop below 1. The total is money(count * product.price) right in the JSX: it follows from count and is not separate state. That is what the lesson on derived state is about.Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Events · The first component · Props: data from above · Lists and keys
- Continues in: Updating objects and arrays · Derived state · Lifting state up · Effects
- Tasks on this topic: State