Expert · React concepts · topic 58 of 64 · 40 min
State and events
Explanation
What is state?
State is data that a component remembers between renders and that can change over time: the items in a cart, the text typed in a box, whether a menu is open. A normal variable is not enough. It starts again from its first value on every render, and changing it does not tell React to draw the screen again.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}Handling events
You attach an event handler with a camelCase prop such as onClick, onChange or onSubmit. Pass the function itself: onClick={handleClick}. Do not call it: onClick={handleClick()} would run during rendering, not on the click. To pass an argument, wrap the call in an arrow function.
function LikeButton() {
const [liked, setLiked] = useState(false);
function handleClick() {
setLiked(!liked);
}
return (
<button onClick={handleClick} aria-pressed={liked}>
{liked ? 'Liked' : 'Like'}
</button>
);
}
// With an argument:
<button onClick={() => removeItem(item.id)}>Remove</button>State is a snapshot
// count is 0
function handleClick() {
setCount(count + 1); // asks for 0 + 1
setCount(count + 1); // still asks for 0 + 1
setCount(count + 1); // still 0 + 1 -> after the render, count is 1
}
function handleClickFixed() {
setCount((c) => c + 1); // 0 -> 1
setCount((c) => c + 1); // 1 -> 2
setCount((c) => c + 1); // 2 -> 3 -> after the render, count is 3
}Never mutate state: make new copies
React checks whether state changed by comparing the old and new value with Object.is. If you push into the same array and set it again, React sees the same array and may skip the update. So always build a new array or object. Spread (...), map and filter all return new ones.
| Task | Wrong (changes the old value) | Right (makes a new value) |
|---|---|---|
| Add an item | items.push(item) | setItems([...items, item]) |
| Remove an item | items.splice(index, 1) | setItems(items.filter((i) => i.id !== id)) |
| Change one item | items[0].done = true | setItems(items.map((i) => (i.id === id ? { ...i, done: !i.done } : i))) |
| Change an object field | user.name = 'Asha' | setUser({ ...user, name: 'Asha' }) |
| Change a nested field | user.address.pin = '560001' | setUser({ ...user, address: { ...user.address, pin: '560001' } }) |
Lifting state up
Sometimes two components need the same data. For example, the product list adds to the cart and the header shows the cart count. Move the state up to their closest common parent, then pass the value and the functions that change it down as props. This is called lifting state up.
function Shop() {
const [cart, setCart] = useState([]);
function handleAdd(product) {
setCart((current) => [...current, product]);
}
return (
<>
<Header cartCount={cart.length} />
<ProductList onAdd={handleAdd} />
</>
);
}useReducer for bigger state logic
When many different actions change the same state (add, remove, change quantity, clear), the update code spreads across many handlers. useReducer collects it in one pure function called a reducer. It takes the current state and an action object and returns the next state. Handlers just dispatch (send) actions.
import { useReducer } from 'react';
function cartReducer(cart, action) {
switch (action.type) {
case 'added':
return [...cart, { ...action.item, qty: 1 }];
case 'removed':
return cart.filter((line) => line.id !== action.id);
default:
return cart;
}
}
function Cart() {
const [cart, dispatch] = useReducer(cartReducer, []);
// in a handler:
// dispatch({ type: 'added', item: rice });
// dispatch({ type: 'removed', id: 'p1' });
}Watch the lesson
State and events · 1/7
State is the memory of a component. When state changes, React draws the component again.
Prefer a video by a teacher? Find free videos on “react useState useReducer tutorial state snapshot immutable updates” on YouTube ↗
In real life: A kirana billing app
A shop owner's billing app keeps the bill in state managed by a reducer with actions like added, qtyChanged and cleared. The bill state lives in the BillingScreen component (lifted up), so ProductSearch can add items and BillTotal can show the sum in rupees. Each change creates a new array, so React always notices it and redraws the total.
Practice (0/3 done)
React keeps a queue of the updates you asked for and applies them in order. Each update is either a plain value (the new state) or an updater function (it gets the previous state and returns the next). Write applyUpdates(initial, updates) that starts from initial, applies every update in order, and returns the final state. Examples: applyUpdates(0, [1, 1, 1]) returns 1 (like calling setCount(count + 1) three times when count is 0). applyUpdates(0, [c => c + 1, c => c + 1, c => c + 1]) returns 3.
Write a reducer cartReducer(state, action) for a shopping cart. state is an array of lines { id, name, price, qty }. Handle these actions and NEVER change the old state or old line objects: (1) { type: "added", item } - if a line with item.id exists, return a new array where that line has qty + 1; otherwise add { ...item, qty: 1 } at the end. (2) { type: "removed", id } - remove that line. (3) { type: "qtyChanged", id, qty } - set that line's qty; if qty is 0 or less, remove the line. (4) { type: "cleared" } - return an empty array. For any other type, return the same state.
In your Vite project, build a small shop with lifted state. A Shop component owns the cart with useReducer and your cartReducer. A ProductList shows 4 products with an "Add" button each, a Header shows the cart count, and a CartPanel lists the lines with + and - buttons that dispatch qtyChanged. Open React DevTools and watch the state change as you click.
Quick quiz
Read more (free)
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.