Building a Headless WooCommerce Storefront with React
WooCommerce powers a large share of the world’s online stores, but its default PHP-rendered frontend shows its age the moment you need custom interactions, a design system, or a mobile app sharing the same data layer. Going headless — decoupling the React frontend from the WordPress backend — lets you keep everything WooCommerce does well (product management, orders, payments, extensions) while building the storefront exactly the way you want.
This tutorial walks through the complete setup using @atomic-solutions/react-woocommerce, a thin React Query wrapper around the WooCommerce Store API. By the end you will have a product grid, a working add-to-cart button, a live cart count in your header, and a clear path to checkout — all driven by typed hooks with no hand-rolled fetch logic.
Why headless WooCommerce makes sense in 2026
The WooCommerce Store API (formerly Cart & Checkout Blocks API) is stable, CORS-friendly, and ships with every modern WooCommerce install. That means you do not need a separate REST proxy or a custom plugin just to read products and write to the cart. The missing piece has always been a good client-side data layer — one that handles caching, background refetching, optimistic updates, and loading states without you writing that infrastructure from scratch. That is exactly what @atomic-solutions/react-woocommerce provides on top of React Query.
For a deeper look at what the package exposes, see the full hook reference.
Installation
pnpm add @atomic-solutions/react-woocommerce
React Query (@tanstack/react-query) is a peer dependency. If your project does not already have it:
pnpm add @tanstack/react-query
Wrapping your app with WooCommerceProvider
The WooCommerceProvider sets up a React Query client, stores your store’s base URL, and wires up the built-in analytics event system. Place it high in your component tree, ideally alongside your existing query client setup.
import { WooCommerceProvider, useProducts } from '@atomic-solutions/react-woocommerce'
function App() {
return (
<WooCommerceProvider baseURL="https://your-store.com/wp-json">
<ProductGrid />
</WooCommerceProvider>
)
}
baseURL should point to the WordPress REST API root. The provider appends /wc/store/v1 for all Store API calls automatically.
Displaying products
With the provider in place, useProducts gives you a paginated, cached product list. The hook accepts the same query parameters as the Store API — per_page, category, search, orderby, and so on.
function ProductGrid() {
const { data: products, isLoading } = useProducts({ per_page: 12 })
if (isLoading) return <div>Loading products…</div>
return (
<div className="grid grid-cols-3 gap-4">
{products?.data.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}
products.data is the typed array of Store API product objects. Each item includes id, name, prices, images, short_description, stock_status, and the full attribute/variation tree — everything you need to render a card without additional requests.
For a single product page, swap in useProduct(id). It fetches from the same cache namespace, so navigating from a product grid to a product detail page is instant when the item was already loaded.
Adding to cart
Cart state is server-authoritative in WooCommerce — the cart lives in a session on the WordPress server, not in local storage. useCart keeps your UI in sync with that session and useAddToCart writes back to it.
import { useCart, useAddToCart } from '@atomic-solutions/react-woocommerce'
function AddToCartButton({ productId }: { productId: number }) {
const { data: cart } = useCart()
const addToCart = useAddToCart()
return (
<button
onClick={() => addToCart.mutate({ id: productId, quantity: 1 })}
disabled={addToCart.isPending}
>
{addToCart.isPending ? 'Adding…' : `Add to cart (${cart?.items_count ?? 0})`}
</button>
)
}
useAddToCart is a React Query mutation. On success it automatically invalidates the useCart query, so the cart count updates across every component that subscribes to it without any manual state management.
For variations, pass variation_id alongside id. For bundles or composite products, the same shape extends naturally since you are just passing Store API parameters through.
Showing the live cart count in your header
Because useCart reads from the shared React Query cache, you can call it in your header component and it will reflect mutations made anywhere else in the tree — no context threading required.
import { useCart } from '@atomic-solutions/react-woocommerce'
function CartIcon() {
const { data: cart } = useCart()
const count = cart?.items_count ?? 0
return (
<a href="/cart" className="relative">
<ShoppingBagIcon />
{count > 0 && (
<span className="absolute -top-1 -right-1 rounded-full bg-black px-1.5 py-0.5 text-xs text-white">
{count}
</span>
)}
</a>
)
}
The badge appears as soon as an item is added and disappears when the cart is emptied — no extra wiring needed.
Updating and removing cart items
Alongside useAddToCart, the package ships useUpdateCartItem and useRemoveCartItem. Both follow the same mutation pattern and both invalidate useCart on success.
const updateItem = useUpdateCartItem()
const removeItem = useRemoveCartItem()
// Increase quantity
updateItem.mutate({ item_key: item.key, quantity: item.quantity + 1 })
// Remove entirely
removeItem.mutate({ item_key: item.key })
item.key comes from the cart items array returned by useCart. Each item carries a stable key for the lifetime of the session.
Checkout flow
When the customer is ready to pay, useCheckout fetches the current checkout state (billing, shipping, payment methods) and usePlaceOrder submits the order. The package fires the built-in analytics events automatically at the right moments:
add_to_cartfires insideuseAddToCarton successbegin_checkoutfires whenuseCheckoutfirst loadspurchasefires insideusePlaceOrderon success
These events follow the GA4 ecommerce schema, so wiring them to Google Analytics, Segment, or any other analytics platform is a matter of listening to the event emitter the provider exposes rather than scattering gtag() calls through your components.
For orders after placement, useOrders provides the authenticated order history — useful for account pages if you are running a full headless account experience.
What you have now
In a few hundred lines of component code you have:
- A cached, paginated product grid backed by the WooCommerce Store API
- A cart that stays in sync with the server session
- A live cart count badge that updates across the tree
- Mutation hooks for add, update, and remove with automatic cache invalidation
- Analytics events fired at the right moments without manual instrumentation
- A typed checkout and order placement path when you are ready to build that screen
The underlying WooCommerce store keeps doing what it does — managing stock, running promotions, processing payments through your existing gateway — while your React frontend is free to look and feel exactly the way your brand needs.
Need help shipping a headless WooCommerce storefront in production? Atomic Solutions builds headless WooCommerce storefronts for clients.