Building Cart State the Right Way in Hydrogen

Introduction

Managing cart state is one of the trickiest parts of building a Hydrogen storefront. Developers often struggle with global state, edge safety, and performance.

This post explores cart store patterns — from lightweight stores to hooks and LocalStorage — that make carts reliable, fast, and developer-friendly.

The Cart Problem in Hydrogen

  • ❌ Shopify APIs don’t provide built-in global cart state.
  • ❌ Overuse of useState → re-renders and drift.
  • ❌ LocalStorage-only carts → lose sync with backend.

👉 The solution: cart stores that balance local speed with API truth.

Pattern 1: Tiny Store with useSyncExternalStore

Hydrogen provides React-friendly primitives like useSyncExternalStore.

let cartState = { lines: [] }; export const cartStore = { subscribe: (listener) => { /* subscribe logic */ }, getSnapshot: () => cartState, update: (newCart) => { cartState = newCart; }, };

const cart = useSyncExternalStore( cartStore.subscribe, cartStore.getSnapshot );

👉 Keeps cart globally in sync without excessive re-renders.

Pattern 2: React Hooks

Wrap cart logic in reusable hooks:

export function useCart() { const [cart, setCart] = useState(null); const addItem = async (variantId) => { const response = await fetch('/api/cart.add', { method: 'POST', body: { variantId } }); setCart(await response.json()); }; return { cart, addItem }; }

👉 Encapsulates mutations, ensures API calls stay consistent.

Pattern 3: LocalStorage + Server Sync

  • LocalStorage provides instant cart state → UI never feels empty.
  • Server API syncs cart on mount.
  • Best of both worlds: fast UI + canonical server cart.

Example: Cart Drawer Component

function CartDrawer() { const { cart, addItem } = useCart(); return ( <aside> {cart?.lines.map(line => ( <div key={line.id}>{line.merchandise.title}</div> ))} <button onClick={() => addItem("gid://variant/123")}>Add</button> </aside> ); }

Case Example: Apparel Brand

  • Initial cart → LocalStorage-only, frequent desync issues.
  • Migrated to Tiny Store + LocalStorage fallback.
  • Outcome: faster cart drawer, fewer API errors, +6% checkout completions.

Guardrails

  • ✅ Always sync local cart with Shopify’s Cart API.
  • ✅ Use useSyncExternalStore for global consistency.
  • ✅ Avoid over-hydration (cart doesn’t need 100 props).
  • ✅ Fallback gracefully if cart fetch fails.

Conclusion

Cart state in Hydrogen doesn’t need to be messy. With patterns like Tiny Store, hooks, and LocalStorage + sync, developers can create carts that are fast, reliable, and easy to maintain.

Treat cart state like a heartbeat — fast locally, canonical at the server.