Cart

The cart system - state management, optimistic updates, server actions, and Shopify integration.

vercel.shop/cart
S
DCart
DCart items

Note: The cart ID is stored in a shopify_cartId cookie with a 7-day expiry. Sessions longer than 7 days start a fresh cart.

The cart system spans the entire storefront, not just the cart page. A React Context provider wraps the app in layout.tsx, giving every component access to cart state. Mutations flow through server actions that call Shopify's GraphQL API, while optimistic updates keep the UI responsive during network round-trips.

Cart context

The CartProvider in context.tsx exposes a useCart() hook with the following state:

  • cart - the last confirmed cart from the server
  • cartWithPending - the confirmed cart merged with optimistic line items, used for rendering
  • isOverlayOpen - controls the cart slideover visibility
  • isAddingToCart - true while an add-to-cart request is in flight
  • isUpdatingCart - true while a quantity update or removal is in flight
  • pendingQuantity - accumulated quantity from rapid add-to-cart clicks before the debounce fires

The provider tracks an originalCart snapshot for rollback if a mutation fails.

Optimistic updates

All cart mutations update the UI before the server responds. The implementation uses two distinct debounce strategies:

Add to cart - trailing-edge debounce at 400ms. Rapid clicks accumulate into pendingQuantity and display as an optimistic line via OptimisticProductInfo. When the debounce fires, a single request sends the accumulated quantity.

Update quantity - leading-edge debounce. The first change fires immediately so the user sees instant feedback. Subsequent changes within the debounce window are batched. Removals (quantity set to 0) bypass debounce entirely and fire immediately.

Each cart line tracks a request ID that increments on every mutation. When a server response arrives, it is only applied if its request ID matches the latest - stale responses from earlier requests are discarded. This prevents race conditions where a slow response overwrites a newer state.

Hard requirement: Every cart mutation must call invalidateCartCache() from @/lib/cart/server. Without it, the cached cart data goes stale and the UI shows outdated state after the next page navigation.

Server actions

All cart mutations are defined in lib/cart/action.ts with "use server":

  • addToCartAction() - adds line items and returns the updated cart
  • updateCartQuantityAction() - validates quantity (1–99) and updates the line
  • removeFromCartAction() - removes a line item and fetches the updated cart
  • syncCartLocaleAction() - updates buyer identity for locale and currency switching
  • updateCartNoteAction() - updates the cart note field
  • buyNowAction() - adds an item and returns the Shopify checkout URL
  • prepareCheckoutAction() - retrieves the checkout URL for an existing cart

Cart overlay

The overlay renders as a right-side Sheet on every viewport, inheriting the Sheet primitive's left/right defaults: w-[calc(100%-2.5rem)] max-w-md. That keeps a consistent gap-10 of backdrop visible on mobile and caps the slideover at 28rem on larger displays — the same width rule used by the mobile menu and the collection filter sidebar.

The overlay content in overlay-content.tsx composes three sections: an empty state when the cart has no items, a scrollable list of line items, and a summary with subtotal, checkout button, and a link to the full cart page.

Overlay items

Each line item in overlay-item.tsx reads from cartWithPending to reflect optimistic state. It renders the product image, title, variant options (color, size, etc.), quantity controls (+/- buttons), a remove button, and the line total. Quantity changes and removals dispatch through the cart context, which handles debouncing and server action calls.

Cart page

The full cart page at /cart uses a 12-column grid layout on large screens: 9 columns for the items list and 3 columns for a sticky summary sidebar. The page is wrapped in a Suspense boundary with a skeleton fallback. Components live in the components/cart-page/ directory:

  • Header - page title with item count badge
  • CartItemsList - renders cart line items (reuses OverlayItem from the overlay)
  • Summary - order subtotal, taxes/shipping note, and checkout button
  • Empty - a server component shown when the cart has no items

Below the items, a RelatedProductsSection shows product recommendations based on the first cart item. When the cart is empty, the Empty component replaces the entire grid.

Shopify integration

Cart operations in lib/shopify/operations/cart.ts use GraphQL mutations: cartLinesAdd, cartLinesUpdate, and cartLinesRemove. The cart ID is stored in a shopify_cartId HTTP-only cookie with a 7-day expiry. Every mutation calls invalidateCartCache() to ensure subsequent reads reflect the latest state.

The transform layer in lib/shopify/transforms/cart.ts converts Shopify's GraphQL response shape into a domain Cart type used throughout the application. This isolates the rest of the codebase from Shopify's API structure.

Key files

FilePurpose
components/cart/context.tsxCart state, optimistic updates, debouncing
components/cart/overlay.tsxCart slideover composition
app/cart/page.tsxFull cart page route
lib/cart/action.tsServer actions for cart mutations
lib/cart/server.tsinvalidateCartCache() — must be called by every cart mutation
lib/shopify/operations/cart.tsShopify cart GraphQL operations

Sub-components (overlay-content, overlay-item, overlay-summary, context-sync, and the pieces under components/cart-page/) live alongside these and are discoverable by directory.

Chat

Tip: You can open and close chat with I

0 / 1000