A cart has one editable collection and several values derived from it. Build the React version with immutable state updates, quantity controls, removal, live line totals, and a live grand total.
Implement the App component in App.tsx. It receives no props and renders the provided three-item cart.
$49.00 x1, Mouse $25.50 x2, Monitor $199.99 x1 gives lines $49.00, $51.00, $199.99 and a grand total of $299.99.+ on Mouse and its qty becomes 3, its line becomes $76.50, and the grand total becomes $325.49.1; quantity never falls below 1.Your cart is empty with a $0.00 total.initialItems in useState; derive line totals and the grand total during render.map for quantity changes and filter for removal. Clamp decrement with Math.max(1, qty - 1).<output>, announce the total atomically, and give the empty message role="status".Hold the whole cart in one items array and let every price on screen be derived from it — so a click only ever edits the array.
A cart is a list of rows, each with a price and a quantity. Tapping +/− changes a quantity; Remove drops a row. From those quantities you show two kinds of money: each line's price × qty, and the grand total across all lines. The trap is treating the totals as things to store and update alongside the quantities — they're not, they're computed.
There is exactly one piece of state: the items array. The line totals and the grand total are not stored anywhere — they're recomputed on every render from items. A click doesn't touch any total; it produces a new items array (bump a qty, clamp it at 1, or filter a row out), React re-renders, and the numbers fall out of the fresh array.
A common first try stores the totals in state next to the items:
const [items, setItems] = useState(initialItems);
const [total, setTotal] = useState(299.99); // duplicated truth
function inc(id) {
setItems(items.map((it) => (it.id === id ? { ...it, qty: it.qty + 1 } : it)));
setTotal(total + /* which price? */ 0); // now you're hand-syncing money
}
It falls apart immediately: every handler now has to remember to also patch total, the per-line totals still aren't stored anywhere, and after a Remove the number is stale. Storing derived values means every edit has two places to update and they drift apart.
import { useState } from 'react';
import './styles.css';
const money = (n: number) => `$${n.toFixed(2)}`;
export default function App() {
const [items, setItems] = useState([
{ id: 1, name: 'Keyboard', price: 49.0, qty: 1 },
{ id: 2, name: 'Mouse', price: 25.5, qty: 2 },
{ id: 3, name: 'Monitor', price: 199.99, qty: 1 },
]);
const setQty = (id: number, delta: number) =>
setItems((prev) =>
prev.map((it) =>
it.id === id ? { ...it, qty: Math.max(1, it.qty + delta) } : it,
),
);
const remove = (id: number) =>
setItems((prev) => prev.filter((it) => it.id !== id));
const total = items.reduce((sum, it) => sum + it.price * it.qty, 0);
return (
<main className="container">
<h1>Shopping Cart</h1>
{items.length === 0 && (
<p className="empty" role="status">Your cart is empty</p>
)}
{items.map((it) => (
<div className="row" key={it.id}>
<span className="name">{it.name}</span>
<div className="stepper">
<button
type="button"
className="qbtn"
aria-label={`Decrease ${it.name} quantity`}
disabled={it.qty === 1}
onClick={() => setQty(it.id, -1)}
>
−
</button>
<output className="qty" aria-label={`${it.name} quantity`} aria-live="polite">
{it.qty}
</output>
<button
type="button"
className="qbtn"
aria-label={`Increase ${it.name} quantity`}
onClick={() => setQty(it.id, 1)}
>
+
</button>
</div>
<span className="line">{money(it.price * it.qty)}</span>
<button
type="button"
className="remove"
aria-label={`Remove ${it.name} from cart`}
onClick={() => remove(it.id)}
>
Remove
</button>
</div>
))}
<div className="total" aria-live="polite" aria-atomic="true">
<span>Total</span>
<span>{money(total)}</span>
</div>
</main>
);
}
items is the only state. setQty and remove return brand-new arrays (map/filter with a spread for the changed row) so React sees the change and never mutates in place. total is a plain const recomputed each render — no useState, no syncing. When items is empty the total naturally reduces to 0 and the status message shows.
1 / 2 / 1, lines $49.00 / $51.00 / $199.99, total $299.99.+ on Mouse: setQty(2, +1) maps to a new array with Mouse at qty 3. Re-render: its line recomputes to 25.5 * 3 = $76.50, and reduce sums the fresh lines to $325.49.1, so its decrement button is disabled and the handler cannot drive it below the floor.filter drops it; two rows remain and the total recomputes without it.reduce on every render instead.it.qty++ then setItems(items) passes the same array reference; React bails on the re-render. Spread a new object: { ...it, qty: ... }.− must clamp with Math.max(1, qty - 1); without it, quantities hit 0 (or go negative) and the line total lies.items to localStorage in an effect so a refresh keeps the cart.useReducer — as actions grow (add, clear, coupon), fold inc/dec/remove into a reducer keyed by action type.A reducer centralizes immutable cart transitions while all displayed money remains derived during rendering.
import { useReducer } from 'react'; import './styles.css';
type Item = { id: number; name: string; price: number; qty: number }; type Action = { type: 'qty'; id: number; delta: number } | { type: 'remove'; id: number };
const initial: Item[] = [{ id: 1, name: 'Keyboard', price: 49, qty: 1 }, { id: 2, name: 'Mouse', price: 25.5, qty: 2 }, { id: 3, name: 'Monitor', price: 199.99, qty: 1 }]; const money = (value: number) => `$${value.toFixed(2)}`;
function reducer(items: Item[], action: Action) { return action.type === 'remove' ? items.filter((item) => item.id !== action.id) : items.map((item) => item.id === action.id ? { ...item, qty: Math.max(1, item.qty + action.delta) } : item); }
export default function App() { const [items, dispatch] = useReducer(reducer, initial); const total = items.reduce((sum, item) => sum + item.price * item.qty, 0); return <main className="container"><h1>Shopping Cart</h1>{items.length === 0 && <p className="empty" role="status">Your cart is empty</p>}{items.map((item) => <div className="row" key={item.id}><span className="name">{item.name}</span><div className="stepper"><button type="button" className="qbtn" aria-label={`Decrease ${item.name} quantity`} disabled={item.qty === 1} onClick={() => dispatch({ type: 'qty', id: item.id, delta: -1 })}>−</button><output className="qty" aria-label={`${item.name} quantity`} aria-live="polite">{item.qty}</output><button type="button" className="qbtn" aria-label={`Increase ${item.name} quantity`} onClick={() => dispatch({ type: 'qty', id: item.id, delta: 1 })}>+</button></div><span className="line">{money(item.price * item.qty)}</span><button type="button" className="remove" aria-label={`Remove ${item.name} from cart`} onClick={() => dispatch({ type: 'remove', id: item.id })}>Remove</button></div>)}<div className="total" aria-live="polite" aria-atomic="true"><span>Total</span><span>{money(total)}</span></div></main>; }A focused hook exposes the collection, derived total, and stable commands without changing the rendered cart.
import { useState } from 'react'; import './styles.css';
const money = (value: number) => `$${value.toFixed(2)}`; const seed = [{ id: 1, name: 'Keyboard', price: 49, qty: 1 }, { id: 2, name: 'Mouse', price: 25.5, qty: 2 }, { id: 3, name: 'Monitor', price: 199.99, qty: 1 }];
function useCart() { const [items, setItems] = useState(seed); const change = (id: number, delta: number) => setItems((current) => current.map((item) => item.id === id ? { ...item, qty: Math.max(1, item.qty + delta) } : item)); const remove = (id: number) => setItems((current) => current.filter((item) => item.id !== id)); return { items, total: items.reduce((sum, item) => sum + item.price * item.qty, 0), change, remove }; }
export default function App() { const cart = useCart(); return <main className="container"><h1>Shopping Cart</h1>{cart.items.length === 0 && <p className="empty" role="status">Your cart is empty</p>}{cart.items.map((item) => <div className="row" key={item.id}><span className="name">{item.name}</span><div className="stepper"><button type="button" className="qbtn" aria-label={`Decrease ${item.name} quantity`} disabled={item.qty === 1} onClick={() => cart.change(item.id, -1)}>−</button><output className="qty" aria-label={`${item.name} quantity`} aria-live="polite">{item.qty}</output><button type="button" className="qbtn" aria-label={`Increase ${item.name} quantity`} onClick={() => cart.change(item.id, 1)}>+</button></div><span className="line">{money(item.price * item.qty)}</span><button type="button" className="remove" aria-label={`Remove ${item.name} from cart`} onClick={() => cart.remove(item.id)}>Remove</button></div>)}<div className="total" aria-live="polite" aria-atomic="true"><span>Total</span><span>{money(cart.total)}</span></div></main>; }Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
A cart has one editable collection and several values derived from it. Build the React version with immutable state updates, quantity controls, removal, live line totals, and a live grand total.
Implement the App component in App.tsx. It receives no props and renders the provided three-item cart.
$49.00 x1, Mouse $25.50 x2, Monitor $199.99 x1 gives lines $49.00, $51.00, $199.99 and a grand total of $299.99.+ on Mouse and its qty becomes 3, its line becomes $76.50, and the grand total becomes $325.49.1; quantity never falls below 1.Your cart is empty with a $0.00 total.initialItems in useState; derive line totals and the grand total during render.map for quantity changes and filter for removal. Clamp decrement with Math.max(1, qty - 1).<output>, announce the total atomically, and give the empty message role="status".Hold the whole cart in one items array and let every price on screen be derived from it — so a click only ever edits the array.
A cart is a list of rows, each with a price and a quantity. Tapping +/− changes a quantity; Remove drops a row. From those quantities you show two kinds of money: each line's price × qty, and the grand total across all lines. The trap is treating the totals as things to store and update alongside the quantities — they're not, they're computed.
There is exactly one piece of state: the items array. The line totals and the grand total are not stored anywhere — they're recomputed on every render from items. A click doesn't touch any total; it produces a new items array (bump a qty, clamp it at 1, or filter a row out), React re-renders, and the numbers fall out of the fresh array.
A common first try stores the totals in state next to the items:
const [items, setItems] = useState(initialItems);
const [total, setTotal] = useState(299.99); // duplicated truth
function inc(id) {
setItems(items.map((it) => (it.id === id ? { ...it, qty: it.qty + 1 } : it)));
setTotal(total + /* which price? */ 0); // now you're hand-syncing money
}
It falls apart immediately: every handler now has to remember to also patch total, the per-line totals still aren't stored anywhere, and after a Remove the number is stale. Storing derived values means every edit has two places to update and they drift apart.
import { useState } from 'react';
import './styles.css';
const money = (n: number) => `$${n.toFixed(2)}`;
export default function App() {
const [items, setItems] = useState([
{ id: 1, name: 'Keyboard', price: 49.0, qty: 1 },
{ id: 2, name: 'Mouse', price: 25.5, qty: 2 },
{ id: 3, name: 'Monitor', price: 199.99, qty: 1 },
]);
const setQty = (id: number, delta: number) =>
setItems((prev) =>
prev.map((it) =>
it.id === id ? { ...it, qty: Math.max(1, it.qty + delta) } : it,
),
);
const remove = (id: number) =>
setItems((prev) => prev.filter((it) => it.id !== id));
const total = items.reduce((sum, it) => sum + it.price * it.qty, 0);
return (
<main className="container">
<h1>Shopping Cart</h1>
{items.length === 0 && (
<p className="empty" role="status">Your cart is empty</p>
)}
{items.map((it) => (
<div className="row" key={it.id}>
<span className="name">{it.name}</span>
<div className="stepper">
<button
type="button"
className="qbtn"
aria-label={`Decrease ${it.name} quantity`}
disabled={it.qty === 1}
onClick={() => setQty(it.id, -1)}
>
−
</button>
<output className="qty" aria-label={`${it.name} quantity`} aria-live="polite">
{it.qty}
</output>
<button
type="button"
className="qbtn"
aria-label={`Increase ${it.name} quantity`}
onClick={() => setQty(it.id, 1)}
>
+
</button>
</div>
<span className="line">{money(it.price * it.qty)}</span>
<button
type="button"
className="remove"
aria-label={`Remove ${it.name} from cart`}
onClick={() => remove(it.id)}
>
Remove
</button>
</div>
))}
<div className="total" aria-live="polite" aria-atomic="true">
<span>Total</span>
<span>{money(total)}</span>
</div>
</main>
);
}
items is the only state. setQty and remove return brand-new arrays (map/filter with a spread for the changed row) so React sees the change and never mutates in place. total is a plain const recomputed each render — no useState, no syncing. When items is empty the total naturally reduces to 0 and the status message shows.
1 / 2 / 1, lines $49.00 / $51.00 / $199.99, total $299.99.+ on Mouse: setQty(2, +1) maps to a new array with Mouse at qty 3. Re-render: its line recomputes to 25.5 * 3 = $76.50, and reduce sums the fresh lines to $325.49.1, so its decrement button is disabled and the handler cannot drive it below the floor.filter drops it; two rows remain and the total recomputes without it.reduce on every render instead.it.qty++ then setItems(items) passes the same array reference; React bails on the re-render. Spread a new object: { ...it, qty: ... }.− must clamp with Math.max(1, qty - 1); without it, quantities hit 0 (or go negative) and the line total lies.items to localStorage in an effect so a refresh keeps the cart.useReducer — as actions grow (add, clear, coupon), fold inc/dec/remove into a reducer keyed by action type.A reducer centralizes immutable cart transitions while all displayed money remains derived during rendering.
import { useReducer } from 'react'; import './styles.css';
type Item = { id: number; name: string; price: number; qty: number }; type Action = { type: 'qty'; id: number; delta: number } | { type: 'remove'; id: number };
const initial: Item[] = [{ id: 1, name: 'Keyboard', price: 49, qty: 1 }, { id: 2, name: 'Mouse', price: 25.5, qty: 2 }, { id: 3, name: 'Monitor', price: 199.99, qty: 1 }]; const money = (value: number) => `$${value.toFixed(2)}`;
function reducer(items: Item[], action: Action) { return action.type === 'remove' ? items.filter((item) => item.id !== action.id) : items.map((item) => item.id === action.id ? { ...item, qty: Math.max(1, item.qty + action.delta) } : item); }
export default function App() { const [items, dispatch] = useReducer(reducer, initial); const total = items.reduce((sum, item) => sum + item.price * item.qty, 0); return <main className="container"><h1>Shopping Cart</h1>{items.length === 0 && <p className="empty" role="status">Your cart is empty</p>}{items.map((item) => <div className="row" key={item.id}><span className="name">{item.name}</span><div className="stepper"><button type="button" className="qbtn" aria-label={`Decrease ${item.name} quantity`} disabled={item.qty === 1} onClick={() => dispatch({ type: 'qty', id: item.id, delta: -1 })}>−</button><output className="qty" aria-label={`${item.name} quantity`} aria-live="polite">{item.qty}</output><button type="button" className="qbtn" aria-label={`Increase ${item.name} quantity`} onClick={() => dispatch({ type: 'qty', id: item.id, delta: 1 })}>+</button></div><span className="line">{money(item.price * item.qty)}</span><button type="button" className="remove" aria-label={`Remove ${item.name} from cart`} onClick={() => dispatch({ type: 'remove', id: item.id })}>Remove</button></div>)}<div className="total" aria-live="polite" aria-atomic="true"><span>Total</span><span>{money(total)}</span></div></main>; }A focused hook exposes the collection, derived total, and stable commands without changing the rendered cart.
import { useState } from 'react'; import './styles.css';
const money = (value: number) => `$${value.toFixed(2)}`; const seed = [{ id: 1, name: 'Keyboard', price: 49, qty: 1 }, { id: 2, name: 'Mouse', price: 25.5, qty: 2 }, { id: 3, name: 'Monitor', price: 199.99, qty: 1 }];
function useCart() { const [items, setItems] = useState(seed); const change = (id: number, delta: number) => setItems((current) => current.map((item) => item.id === id ? { ...item, qty: Math.max(1, item.qty + delta) } : item)); const remove = (id: number) => setItems((current) => current.filter((item) => item.id !== id)); return { items, total: items.reduce((sum, item) => sum + item.price * item.qty, 0), change, remove }; }
export default function App() { const cart = useCart(); return <main className="container"><h1>Shopping Cart</h1>{cart.items.length === 0 && <p className="empty" role="status">Your cart is empty</p>}{cart.items.map((item) => <div className="row" key={item.id}><span className="name">{item.name}</span><div className="stepper"><button type="button" className="qbtn" aria-label={`Decrease ${item.name} quantity`} disabled={item.qty === 1} onClick={() => cart.change(item.id, -1)}>−</button><output className="qty" aria-label={`${item.name} quantity`} aria-live="polite">{item.qty}</output><button type="button" className="qbtn" aria-label={`Increase ${item.name} quantity`} onClick={() => cart.change(item.id, 1)}>+</button></div><span className="line">{money(item.price * item.qty)}</span><button type="button" className="remove" aria-label={`Remove ${item.name} from cart`} onClick={() => cart.remove(item.id)}>Remove</button></div>)}<div className="total" aria-live="polite" aria-atomic="true"><span>Total</span><span>{money(cart.total)}</span></div></main>; }Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.