State and events
Make the Brew & Bean page interactive with useState and event handlers. Build a working cart, learn why state must never be mutated, and see how to lift state up.
In this lesson you will make the Brew & Bean page interactive: a counter first, then a working cart where customers add and remove items and see a live total. You will learn useState, event handlers, and how to share state between components.
Prerequisites: the project from *Props and composition*. You should be comfortable with props, children, and with array methods like map and reduce and the spread operator from *Modern JavaScript for React*.
Why props are not enough
Props come from outside and cannot be changed by the component. But a cart changes while the user is on the page. A component needs memory of its own, and when that memory changes, the screen must update.
A normal variable does not work for this, for two reasons: it is reset every time the component function runs again, and changing it does not tell React to redraw. State solves both. It is a value React remembers between renders and redraws the component when you change it.
useState: the basics
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Cups ordered: {count}</p>
<button onClick={() => setCount(count + 1)}>Add a cup</button>
</div>
);
}useState(0) gives back a pair: the current value (count, starting at 0) and a function to change it (setCount). Clicking calls setCount, React re-runs Counter with the new value and updates the page.
Read the first line carefully: const [count, setCount] = useState(0);. This is array destructuring (you met it in the JavaScript lesson). The names are yours. The convention is [thing, setThing].
useState is a hook: a function whose name starts with use that lets a component use a React feature. Hooks have two rules: call them only at the top level of a component (not inside if, loops or nested functions), and only inside components or other hooks.
Try it
- 1Create
src/Counter.jsxwith the code above. - 2In
src/App.jsxaddimport Counter from "./Counter.jsx";and put<Counter />inside<main>. - 3Click the button. The number goes up.
- 4Add a second
<Counter />and click each one. They count independently: every copy of a component has its own state.
How an update really works
The render cycle
- 1You click. React calls your
onClickfunction. - 2
setCount(count + 1)does not changecountright now. It asks React to re-render with a new value. - 3React calls the
Counterfunction again. This timeuseStatereturns the new value, socountis 1. - 4React compares the new JSX with the old and updates only what changed in the real page.
setCount is not an assignment
State updates are not instant inside the same event. After setCount(count + 1), the variable count in that same function still has the old value. Calling setCount(count + 1) three times in a row adds only 1, because each call uses the same old count.
// adds 3 (correct): each update receives the latest value
function addThree() {
setCount((current) => current + 1);
setCount((current) => current + 1);
setCount((current) => current + 1);
}Passing a function to the setter is the safe way whenever the new value depends on the old one. It is called the functional update.
Events
You attach behaviour with props named like onClick, onChange, onSubmit, onKeyDown. Give them a function, not the result of calling one.
Passing handlers
| Code | What happens |
|---|---|
| onClick={handleAdd} | Correct: React calls handleAdd when the click happens |
| onClick={() => handleAdd(id)} | Correct: use an arrow function when you need to pass an argument |
| onClick={handleAdd()} | Bug: handleAdd runs immediately during rendering, and its result is used as the handler |
| onClick="handleAdd()" | Bug: JSX wants a function in braces, not a string |
State rule: never change it directly
State holds numbers, strings, booleans, arrays or objects. For arrays and objects you must make a new copy and pass it to the setter. Changing the existing one in place (mutating) does not trigger a re-render, because React sees the same object and thinks nothing changed.
Immutable updates
| Task | Wrong (mutates) | Right (new copy) |
|---|---|---|
| Add to an array | items.push(x); setItems(items) | setItems([...items, x]) |
| Remove from an array | items.splice(i, 1) | setItems(items.filter((it) => it.id !== id)) |
| Change a field of an object | user.name = "A"; setUser(user) | setUser({ ...user, name: "A" }) |
| Change one key of an object by id | cart[id] = 2; setCart(cart) | setCart({ ...cart, [id]: 2 }) |
Build the cart
Now the real feature. Each menu card needs an Add button and a quantity. The total in a cart summary must also change. That means two different components need the same data: the cards (to show and change quantities) and the summary (to show the total).
The rule: lift state up. Put the state in the nearest parent that contains both components, and pass the value and the functions down as props. Here that parent is App.
The cart is an object that maps an item id to a quantity, for example { espresso: 2, "masala-chai": 1 }. An item that is not in the object has quantity 0.
export const menu = [
{ id: "espresso", name: "Espresso", category: "coffee", price: 120 },
{ id: "cappuccino", name: "Cappuccino", category: "coffee", price: 180 },
{ id: "cold-brew", name: "Cold Brew", category: "coffee", price: 220 },
{ id: "filter-coffee", name: "Filter Coffee", category: "coffee", price: 80 },
{ id: "masala-chai", name: "Masala Chai", category: "tea", price: 90 },
{ id: "croissant", name: "Butter Croissant", category: "bakery", price: 150 },
{ id: "banana-bread", name: "Banana Bread", category: "bakery", price: 130 },
{ id: "veg-sandwich", name: "Veg Grilled Sandwich", category: "food", price: 160 },
];The shop's menu as plain data. Later this will come from the GET /menu API.
export default function MenuCard({ name, price, category, quantity, onAdd, onRemove }) {
return (
<article className="card">
<h3>{name}</h3>
<p className="category">{category}</p>
<p className="price">₹{price}</p>
<div className="qty-controls">
<button onClick={onRemove} disabled={quantity === 0} aria-label={`Remove one ${name}`}>
-
</button>
<span>{quantity}</span>
<button onClick={onAdd} aria-label={`Add one ${name}`}>
+
</button>
</div>
</article>
);
}The card owns no state. It receives the quantity and two functions from its parent, and calls them on click. The Remove button is disabled at zero. The aria-label gives screen readers a clear name for the buttons.
export default function CartSummary({ itemCount, total }) {
return (
<div className="cart-summary">
{itemCount === 0 ? (
<p>Your cart is empty. Add something tasty.</p>
) : (
<p>
{itemCount} {itemCount === 1 ? "item" : "items"} in your cart. Total: ₹{total}
</p>
)}
</div>
);
}Shows one of two messages with a ternary, and picks "item" or "items".
import { useState } from "react";
import Header from "./Header.jsx";
import Section from "./Section.jsx";
import MenuCard from "./MenuCard.jsx";
import CartSummary from "./CartSummary.jsx";
import Footer from "./Footer.jsx";
import { menu } from "./menu.js";
export default function App() {
const [cart, setCart] = useState({});
function addItem(id) {
setCart((current) => ({ ...current, [id]: (current[id] ?? 0) + 1 }));
}
function removeItem(id) {
setCart((current) => {
const next = { ...current };
const quantity = (next[id] ?? 0) - 1;
if (quantity > 0) {
next[id] = quantity;
} else {
delete next[id];
}
return next;
});
}
const itemCount = Object.values(cart).reduce((sum, qty) => sum + qty, 0);
const total = menu.reduce((sum, item) => sum + item.price * (cart[item.id] ?? 0), 0);
return (
<>
<Header />
<main className="page">
<CartSummary itemCount={itemCount} total={total} />
<Section title="Menu">
<div className="menu-grid">
{menu.map((item) => (
<MenuCard
key={item.id}
name={item.name}
price={item.price}
category={item.category}
quantity={cart[item.id] ?? 0}
onAdd={() => addItem(item.id)}
onRemove={() => removeItem(item.id)}
/>
))}
</div>
</Section>
</main>
<Footer />
</>
);
}The only state is cart. itemCount and total are not stored; they are calculated from cart on every render. The menu.map loop and the key prop are explained in the next lesson on lists. The ?? 0 means "use 0 if there is no entry".
.qty-controls {
display: flex;
align-items: center;
gap: 12px;
margin-top: 12px;
}
.qty-controls button {
width: 36px;
height: 36px;
font-size: 1.2rem;
border: 1px solid #4a2c1a;
border-radius: 8px;
background: #fff;
color: #4a2c1a;
cursor: pointer;
}
.qty-controls button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.cart-summary {
position: sticky;
top: 0;
margin-bottom: 16px;
background: #fde9c9;
border-radius: 8px;
}A sticky bar at the top of the page saying "Your cart is empty." Eight cards with - and + buttons. Click + on Espresso twice and Masala Chai once: the bar says "3 items in your cart. Total: ₹330". Click - and the numbers go down; at zero the - button greys out.The customer's total here is only a display. When you build the real ordering flow, the server must compute the total itself, as in the Brew & Bean API, and never trust a total sent from the browser.
Derive, don't duplicate
Notice that itemCount and total are ordinary variables calculated from cart. You could have made them extra state, but then you would have to keep three pieces of state in sync, and bugs would follow. A good rule: store the minimum state, and calculate everything else during render.
Is it state?
| Question | If yes |
|---|---|
| Does it change over time because of user action? | It is probably state |
| Is it passed in from a parent? | It is a prop, not state |
| Can it be calculated from other state or props? | Do not store it; calculate it |
| Do two components need it? | Lift it to their closest common parent |
Common mistakes
Mistakes with state
| Mistake | Symptom | Fix |
|---|---|---|
Mutating state (cart[id] = 1) | Screen does not update | Create a new object or array |
onClick={add()} | Runs on every render, often an infinite loop of updates | onClick={add} or onClick={() => add(id)} |
| Reading state right after setting it | Old value in the same function | Use the new value you computed, or the functional update |
Calling a hook inside an if | Error about hooks order | Call hooks at the top of the component |
Copying a prop into state useState(price) | State ignores later prop changes | Use the prop directly unless you really want an editable copy |
In my React component (plain JavaScript, hooks) the screen does not update when I click. Here is the code:
---
[paste the component]
---
Explain which rule of state I am breaking (mutation, stale value, handler called instead of passed, or something else), show the minimal fix, and explain in two sentences why it works.What to expect: A good answer names a specific cause such as mutating an array in place and changes only the lines needed. Test the fix yourself.
Try it: Add a Clear cart button and a Pay-at-pickup note
In App.jsx add a button labelled "Clear cart" that empties the cart, shown only when the cart is not empty. Then change CartSummary so it also shows the line "Pay at pickup" when there is at least one item.
Try it: Stretch: highlight the biggest line
Show the name of the cart item with the highest line total (price times quantity) in the summary, calculated during render without any new state.
Key takeaways
- State is memory that React keeps between renders; changing it re-renders the component.
const [value, setValue] = useState(initial)returns the value and its setter; call hooks at the top level only.- Pass functions to events (
onClick={fn}oronClick={() => fn(id)}), never call them in JSX. - Never mutate state; replace objects and arrays with new copies. Use the functional update when the new value depends on the old.
- Lift state to the closest common parent, pass values and handlers down as props, store the minimum and calculate the rest.
Check yourself
Finishing the quiz marks the lesson as done.
1. Why does a normal let count = 0 variable not work for a counter in a component?
2. Which is the correct way to add an item to an array held in state?
3. What is wrong with <button onClick={handleAdd()}>?
4. Two sibling components both need the cart. Where should the cart state live?
5. total can be computed from cart and menu. What is the best approach?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.