WooCommerce Store API: Build a Custom Storefront with React

Build a custom WooCommerce storefront with the Store API and React

The WooCommerce Store API (/wp-json/wc/store/v1) is the API behind the Cart and Checkout blocks. It is made for the front end: public product data needs no authentication, the cart belongs to the visitor’s session, and it returns prices, images and stock in a form ready for display. That makes it the right base for a custom product listing or a small React storefront, with WooCommerce staying the source of truth for products, prices and orders.

Problem: A theme needs a fast, interactive product grid with add-to-cart and a live cart counter, without the WooCommerce templates and without jQuery.

Solution: Load products from /wc/store/v1/products, add items with POST /wc/store/v1/cart/add-item using the Store API nonce, and keep the nonce and the cart count from the responses.

Pass the settings from PHP

<?php
add_action( 'wp_enqueue_scripts', function () {
    $asset = include get_theme_file_path( 'build/storefront.asset.php' );
    wp_enqueue_script( 'storefront', get_theme_file_uri( 'build/storefront.js' ), $asset['dependencies'], $asset['version'], true );
    wp_localize_script( 'storefront', 'storefrontSettings', array(
        'apiBase' => esc_url_raw( rest_url( 'wc/store/v1' ) ),
    ) );
} );

Built with @wordpress/scripts, the bundle can import React from @wordpress/element, which WordPress already loads.

The API helpers

// src/storefront.js
let nonce = '';

async function storeApi( path, options = {} ) {
    const response = await fetch( storefrontSettings.apiBase + path, {
        credentials: 'same-origin',
        ...options,
        headers: { 'Content-Type': 'application/json', ...( nonce ? { Nonce: nonce } : {} ), ...options.headers },
    } );
    // WooCommerce sends a fresh nonce with every response
    nonce = response.headers.get( 'Nonce' ) || nonce;

    const data = await response.json();
    if ( ! response.ok ) {
        throw new Error( data.message || 'Store API error ' + response.status );
    }
    return data;
}

export const getProducts = ( page = 1 ) => storeApi( `/products?page=${ page }&per_page=12` );
export const getCart = () => storeApi( '/cart' );
export const addToCart = ( id, quantity = 1 ) =>
    storeApi( '/cart/add-item', { method: 'POST', body: JSON.stringify( { id, quantity } ) } );

Write requests (add, remove, update) need the nonce. The simplest way to get it is to call /cart once when the app starts: every Store API response carries the current nonce in the Nonce header.

Prices

Prices come as integers in the minor unit of the currency, with the number of decimals next to them:

export function formatPrice( prices ) {
    const value = Number( prices.price ) / 10 ** prices.currency_minor_unit;
    return prices.currency_prefix + value.toFixed( prices.currency_minor_unit ) + prices.currency_suffix;
}

Dividing by 100 would be wrong for currencies without decimals, such as the yen.

The components

import { createRoot, useState, useEffect } from '@wordpress/element';
import { getProducts, getCart, addToCart, formatPrice } from './api';

function ProductCard( { product, onAdd } ) {
    const [ adding, setAdding ] = useState( false );
    const image = product.images?.[ 0 ];

    const add = async () => {
        setAdding( true );
        try {
            await onAdd( product.id );
        } finally {
            setAdding( false );
        }
    };

    return (
        <article className="product-card">
            { image && <img src={ image.thumbnail } alt={ image.alt } loading="lazy" /> }
            <h3><a href={ product.permalink }>{ product.name }</a></h3>
            <p className="price">{ formatPrice( product.prices ) }</p>
            <button onClick={ add } disabled={ adding || ! product.is_purchasable || ! product.is_in_stock }>
                { adding ? 'Adding…' : 'Add to cart' }
            </button>
        </article>
    );
}

function Storefront() {
    const [ products, setProducts ] = useState( [] );
    const [ count, setCount ] = useState( 0 );
    const [ page, setPage ] = useState( 1 );
    const [ error, setError ] = useState( '' );

    useEffect( () => {
        getCart().then( ( cart ) => setCount( cart.items_count ) ).catch( () => {} );
    }, [] );

    useEffect( () => {
        getProducts( page ).then( setProducts ).catch( ( e ) => setError( e.message ) );
    }, [ page ] );

    const onAdd = async ( id ) => {
        try {
            const cart = await addToCart( id );
            setCount( cart.items_count );
        } catch ( e ) {
            setError( e.message ); // e.g. "out of stock", from WooCommerce
        }
    };

    return (
        <div className="storefront">
            <p className="cart-count">Cart: { count }</p>
            { error && <p className="storefront__error" role="alert">{ error }</p> }
            <div className="product-grid">
                { products.map( ( p ) => <ProductCard key={ p.id } product={ p } onAdd={ onAdd } /> ) }
            </div>
            <nav>
                <button onClick={ () => setPage( ( p ) => Math.max( 1, p - 1 ) ) } disabled={ page === 1 }>← Prev</button>
                <button onClick={ () => setPage( ( p ) => p + 1 ) } disabled={ products.length < 12 }>Next →</button>
            </nav>
        </div>
    );
}

const root = document.getElementById( 'storefront' );
if ( root ) {
    createRoot( root ).render( <Storefront /> );
}

The error messages come from WooCommerce itself (stock limits, sold individually, minimum quantities), so the shop rules are enforced on the server, not in React.

Another domain

On the same domain the cart lives in the WooCommerce session cookie. For a fully decoupled front end on another domain, the Store API also supports a cart token: the Cart-Token response header identifies the cart, and the client sends it back in the same header instead of relying on cookies. The checkout itself can then also go through the Store API (/checkout), but payment gateways must support it.

NOTE: The Store API is public by design: do not add your own private data to its responses through extensions without checking who asks. For admin tasks (creating products, reading all orders) the right API is the authenticated /wc/v3 REST API with Application Passwords, see Application Passwords.