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.