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.

May 15, 20265 min read6 / 7

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.

JSX
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:

JSX
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:

JSX
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:

JSX
{loading ? ( <h2>Loading...</h2> ) : ( <Cart cart={cart} checkout={checkout} /> )}

Cart state in Order, passed down to Cart as props; checkout function flows the same way 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

  1. onSubmit on <form> handles both button clicks and keyboard Enter. More accessible than onClick on a button. Always call e.preventDefault() to stop the page reload.
  2. 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.
  3. 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.
  4. 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.
  5. async/await in event handlers works without any setup. Set loading before the await, clear it after.

Further Reading and Watching