Cart And Immutable State

Pushing to an array does not trigger a Proxy set trap. Cart updates must replace the array entirely. Here is why immutable updates matter when reactivity depends on property assignment.

May 1, 20263 min read4 / 6

Adding an item to the cart with Store.cart.push(item) does not trigger the proxy. The set trap fires when a property is reassigned, not when the value it holds is mutated. This is the same constraint Redux enforces -- and the reason cart updates must create a new array instead of modifying the existing one.

The Essentials

  1. Mutation does not trigger set: array.push(), array.splice(), and any in-place mutation change the array's contents without reassigning the property. The proxy never sees it.
  2. Reassignment does trigger set: Store.cart = [...Store.cart, newItem] replaces the cart property with a new array. The proxy fires.
  3. Update-or-insert logic with filter and spread: Check if the product is already in the cart. If yes, create a new array with the quantity incremented using map. If no, spread the current cart and append the new item.
  4. removeFromCart uses filter: Create a new array containing every item whose ID does not match the one being removed.
  5. Cart total with reduce: Sum quantities across all cart items using reduce with an accumulator, starting at zero.

The Order Service

Cart operations belong in a dedicated service, not inside any component:

JavaScript
// services/Order.js import { getProductById } from './Menu.js'; export async function addToCart(id) { const product = await getProductById(id); const existingItems = app.store.cart.filter(p => p.product.id === id); if (existingItems.length > 0) { app.store.cart = app.store.cart.map(item => item.product.id === id ? { ...item, quantity: item.quantity + 1 } : item ); } else { app.store.cart = [...app.store.cart, { product, quantity: 1 }]; } } export function removeFromCart(id) { app.store.cart = app.store.cart.filter(item => item.product.id !== id); }

Both functions assign to app.store.cart with a new array. The proxy's set trap fires, dispatches app:cartchange, and any listener updates the UI.

Why push Does Not Work

JavaScript
// This does NOT trigger the proxy app.store.cart.push({ product, quantity: 1 }); // This DOES trigger the proxy app.store.cart = [...app.store.cart, { product, quantity: 1 }];

push mutates the array that cart already points to. The cart property itself still references the same array object -- so from the proxy's perspective, nothing changed. Spreading into a new array creates a new object and assigns it to cart, which the proxy intercepts.

This is the same constraint Redux enforces with its "immutable updates" rule. Redux reducers must return new state objects rather than mutating existing ones. The reason is identical: the comparison mechanism (or in this case, the trap) works on references, not deep equality.

Updating the Cart Badge

The cart badge in the navigation shows the total quantity of all items:

JavaScript
// app.js window.addEventListener('app:cartchange', () => { const badge = document.getElementById('badge'); const quantity = app.store.cart.reduce( (total, item) => total + item.quantity, 0 ); badge.textContent = quantity; badge.hidden = quantity === 0; });

reduce accumulates the quantity field across every cart item. Starting at zero, it produces the total count. The badge is hidden when the total is zero so the UI does not display an empty badge on first load.

This listener runs in app.js -- outside any component -- because the badge is part of the global navigation, not any individual page.

The getProductById Function

Adding to the cart by ID requires looking up the full product. The lookup must handle the case where the menu has not loaded yet:

JavaScript
// services/Menu.js export async function getProductById(id) { if (!app.store.menu) { await loadData(); } for (const category of app.store.menu) { for (const product of category.products) { if (product.id == id) return product; } } return null; }

The double loop traverses categories then products within each category. The loose equality (==) compares numeric IDs from the data with string IDs from URL parameters or dataset attributes.

Further Reading and Watching

Video: