User Input And Cart
Submitting a form and managing a growing cart array is where controlled state really earns its keep -- this is the moment a UI starts behaving like a real application.
Custom hooks cleaned up the fetch logic. Now the Order form needs to do something when the customer clicks submit: add a pizza to a cart and eventually send that cart to the API.
Handling Form Submission
The form already has a submit button. Wire up onSubmit on the <form> element, not onClick on the button. The reason: if a user is in a text field and presses Enter, onClick on a button does nothing. onSubmit fires from both the button click and the keyboard Enter. It is also more accessible -- screen readers understand form submission.
const [cart, setCart] = useState([]);
function handleSubmit(e) {
e.preventDefault();
setCart([
...cart,
{
pizza: pizzaTypes.find((p) => p.id === pizzaType),
size: pizzaSize,
price: pizzaTypes.find((p) => p.id === pizzaType)?.sizes[pizzaSize],
},
]);
}
// in the JSX:
<form onSubmit={handleSubmit}>e.preventDefault() stops the browser's default behavior of reloading the page on submit.
The spread [...cart, newItem] creates a new array with the existing items plus the new one. This matters: React state should never be mutated in place. cart.push(newItem) would mutate the existing array. React would not detect the change and would not re-render.
Building the Cart Component
Create src/Cart.jsx. It receives cart and a checkout function as props:
const intl = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
});
const Cart = ({ cart, checkout }) => {
let total = 0;
for (let i = 0; i < cart.length; i++) {
const item = cart[i];
total += item.pizza.sizes[item.size];
}
return (
<div className="cart">
<h2>Cart</h2>
<ul>
{cart.map((item, index) => (
<li key={index}>
<span>{item.size} - </span>
<span>{item.pizza.name} - </span>
<span>{intl.format(item.price)}</span>
</li>
))}
</ul>
<p>Total: {intl.format(total)}</p>
<button type="button" onClick={checkout}>
Checkout
</button>
</div>
);
};
export default Cart;A few things worth noting:
key={index} is intentional here. Cart items have no stable unique ID -- three identical medium pepperoni pizzas are genuinely the same object. Array index is the correct key when items have no unique identity and the list only appends. This is the exception to the keys rule.
A for loop instead of reduce. The total could be written as a reduce. It is shorter. It is also harder to read for developers who are not comfortable with functional patterns. A for loop is explicit and does not require knowing how reduce accumulates. Clever code that every reader can understand is better than clever code that some readers will stare at.
checkout is a button, not a submit. The cart has no form inputs. type="button" prevents this button from accidentally triggering a parent form's submit handler. onClick is correct here.
The Checkout Function
The checkout function lives in Order.jsx because that is where the cart state lives. It sends the cart to the API and then resets state:
async function checkout() {
setLoading(true);
await fetch("/api/order", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ cart }),
});
setCart([]);
setLoading(false);
}await pauses execution at the fetch. Once the API responds, the cart is emptied and loading is turned off. The setLoading(true) at the start prevents the user from adding more items while checkout is in flight.
Pass both the cart and the checkout function down as props:
{loading ? (
<h2>Loading...</h2>
) : (
<Cart cart={cart} checkout={checkout} />
)} ExpandCart state in Order, passed down to Cart as props; checkout function flows the same way
Props Are One-Way and Immutable
The Cart component receives cart and checkout from Order. It cannot modify cart directly -- the array belongs to Order's state. The only way Cart can affect the parent is through the checkout callback that the parent passed down.
This is React's one-way data flow. Data goes from parent to child through props. A child influences a parent only by calling a function the parent gave it. It makes bugs easy to localize: if the cart total is wrong, the problem is either in the data the parent passed or in how Cart displays it. There is no hidden channel where Cart could modify something higher up.
There is one problem left: if the user navigates away from the Order page, the cart disappears because the component unmounts. Solving that -- and showing the cart count in a nav header -- is exactly what useContext is designed for.
The Essentials
onSubmiton<form>handles both button clicks and keyboard Enter. More accessible thanonClickon a button. Always calle.preventDefault()to stop the page reload.- Never mutate state arrays in place.
[...cart, newItem]creates a new array.cart.push(newItem)mutates in place and React will not detect the change. key={index}is correct for cart items because they have no stable unique identity. Array index is acceptable when items only append and carry no internal state.- Pass functions as props to let children trigger parent state changes. The child does not know or care how the function works -- it just calls it.
async/awaitin event handlers works without any setup. Set loading before the await, clear it after.
Further Reading and Watching
- Adding Interactivity (React docs) -- official guide to event handlers, state updates, and the mental model behind React's one-way data flow
- Sharing State Between Components (React docs) -- when and how to lift state up so sibling components can coordinate
- React State Management (Traversy Media) -- practical walkthrough of local state, lifting state, and when context starts to make sense
Keep reading