Skip to main content

Hooks (Primitives)

Headless hooks that manage state and integrate with @shoppexio/storefront. Use these to build custom components or access SDK functionality in your own code.
These hooks are for custom React storefronts outside Shoppex hosting. Hosted Shoppex themes use Liquid templates and the platform commerce runtime.

useCart

Complete cart state management with all CRUD operations.
// @validate
import { useCart } from '@shoppex/ui';

function CartBadge() {
  const {
    items,           // CartItem[]
    itemCount,       // number of unique items
    totalQuantity,   // total quantity across all items
    addToCart,       // (productId, variantId?, qty?, options?) => void
    updateQuantity,  // (productId, variantId, qty) => void
    removeItem,      // (productId, variantId?) => void
    clearCart,       // () => void
    getItemQuantity, // (productId, variantId?) => number
    isInCart,        // (productId, variantId?) => boolean
  } = useCart();

  return <span>{totalQuantity} items in cart</span>;
}

Adding Products

// @validate
import { useCart } from '@shoppex/ui';

function addExamples() {
  const { addToCart } = useCart();

  // Simple add
  addToCart('product-123');

  // With variant and quantity
  addToCart('product-123', 'variant-456', 2);

  // With options
  addToCart('product-123', 'variant-456', 1, {
    addons: [{ id: 'addon-1' }],
    custom_fields: { engraving: 'Hello' },
    price_variant_id: 'tier-premium',
  });

  return null;
}

useCartItem

Cart state for a specific product. Useful for product cards and detail pages.
// @validate
import { useCartItem } from '@shoppex/ui';

function ProductActions({ productId, variantId }) {
  const {
    quantity,    // number
    isInCart,    // boolean
    add,         // (qty?, options?) => void
    remove,      // () => void
    increment,   // () => void
    decrement,   // () => void
    setQuantity, // (qty) => void
  } = useCartItem(productId, variantId);

  if (isInCart) {
    return (
      <div>
        <button onClick={decrement}>-</button>
        <span>{quantity}</span>
        <button onClick={increment}>+</button>
      </div>
    );
  }

  return <button onClick={() => add()}>Add to Cart</button>;
}

useStore

Fetch and cache storefront data (store, products, groups).
// @validate
import { useStore } from '@shoppex/ui';

function Storefront() {
  const {
    store,      // Shop | null
    products,   // Product[]
    groups,     // ProductGroup[]
    isLoading,  // boolean
    error,      // string | null
  } = useStore();

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div role="alert">{error}</div>;
  if (!store) return null;

  return (
    <div>
      <h1>{store.name}</h1>
      <p>{products.length} products</p>
    </div>
  );
}
Data is cached in memory. Subsequent calls return cached data instantly.

useStoreSettings

Extract store configuration flags from shop data.
// @validate
import { useState } from 'react';
import { useStoreSettings } from '@shoppex/ui';

function Header() {
  const {
    isCartEnabled,
    isSearchEnabled,
    isSortEnabled,
    defaultSort,
    shouldHideOutOfStock,
    shouldHideStockCounter,
    isDarkMode,
    shouldCenterProductTitles,
    shouldCenterGroupTitles,
  } = useStoreSettings();

  const [cartOpen, setCartOpen] = useState(false);

  return (
    <header>
      {isSearchEnabled && <button type="button">Search</button>}
      {isCartEnabled && (
        <button type="button" onClick={() => setCartOpen(!cartOpen)}>
          Cart
        </button>
      )}
    </header>
  );
}

useSearch

Product search with debouncing and caching.
// @validate
import { useSearch } from '@shoppex/ui';
import { ProductCard } from '@shoppex/ui';

function SearchPage() {
  const {
    query,        // string
    setQuery,     // (query: string) => void
    results,      // Product[]
    isSearching,  // boolean
    hasSearched,  // boolean
    clear,        // () => void
  } = useSearch({
    debounceMs: 300,      // default: 300
    minQueryLength: 2,    // default: 1
    hideOutOfStock: true, // default: false
    maxResults: 20,       // default: 50
  });

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search products..."
      />
      {isSearching && <div>Searching...</div>}
      {results.map(product => (
        <ProductCard key={product.uniqid} product={product} />
      ))}
    </div>
  );
}

useProductFilter

Filter and sort products by category and price.
// @validate
import { useProductFilter } from '@shoppex/ui';
import { ProductCard, VALID_SORT_KEYS, type SortKey } from '@shoppex/ui';

function ProductListing({ products }) {
  const {
    filtered,     // Product[] - filtered and sorted
    categories,   // string[] - unique categories
    category,     // string | null - current filter
    setCategory,  // (cat: string | null) => void
    sort,         // SortKey
    setSort,      // (key: SortKey) => void
  } = useProductFilter(products);

  return (
    <div>
      {/* Category filters */}
      <button onClick={() => setCategory(null)}>All</button>
      {categories.map(cat => (
        <button
          key={cat}
          onClick={() => setCategory(cat)}
          className={category === cat ? 'active' : ''}
        >
          {cat}
        </button>
      ))}

      {/* Sort dropdown */}
      <select
        value={sort}
        onChange={(e) => {
          const next = e.target.value as SortKey;
          if (VALID_SORT_KEYS.includes(next)) setSort(next);
        }}
      >
        <option value="featured">Featured</option>
        <option value="newest">Newest</option>
        <option value="price-asc">Price: Low to High</option>
        <option value="price-desc">Price: High to Low</option>
      </select>

      {/* Products */}
      {filtered.map(product => (
        <ProductCard key={product.uniqid} product={product} />
      ))}
    </div>
  );
}

useCheckout

Handle checkout flow and coupon validation.
// @validate
import { useCheckout } from '@shoppex/ui';
import { useState } from 'react';

function CheckoutPage() {
  const {
    checkout,        // (options?) => Promise<CheckoutResult>
    validateCoupon,  // (code, options?) => Promise<CouponValidation>
    isLoading,       // boolean
    error,           // string | null
  } = useCheckout();

  const [email, setEmail] = useState('');
  const [coupon, setCoupon] = useState('');

  const handleCheckout = async () => {
    const result = await checkout({
      email,
      coupon,
      autoRedirect: true, // default: true
    });

    if (!result.success) {
      console.error(result.message);
    }
    // If autoRedirect is true, user is redirected to checkout page
  };

  const handleValidateCoupon = async () => {
    const result = await validateCoupon(coupon, {
      productId: 'prod_abc123',
      variantId: 'variant_lifetime',
    });
    if (result.valid) {
      console.log(result.discount, result.discount_type);
    }
  };

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        void handleCheckout();
      }}
    >
      <input
        type="email"
        value={email}
        onChange={e => setEmail(e.target.value)}
        placeholder="Email"
      />
      <input
        value={coupon}
        onChange={e => setCoupon(e.target.value)}
        placeholder="Coupon code"
      />
      <button type="button" onClick={handleValidateCoupon}>
        Apply Coupon
      </button>
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Processing...' : 'Checkout'}
      </button>
    </form>
  );
}

usePrice

Price formatting with store currency.
import { usePrice, useProductPricing } from '@shoppex/ui';

function PriceTag({ amount }) {
  const { format, currency } = usePrice();

  return <span>{format(amount)}</span>; // "$99.99"
}

function ProductPrice({ product }) {
  const pricing = useProductPricing(product);

  return (
    <div>
      {pricing.hasDiscount && (
        <span className="line-through">{pricing.originalPrice}</span>
      )}
      <span>{pricing.price}</span>
      {pricing.isSubscription && (
        <span>/{pricing.subscriptionInterval}</span>
      )}
      {pricing.discountPercentage && (
        <span>-{pricing.discountPercentage}%</span>
      )}
    </div>
  );
}

ProductPricing Type

interface ProductPricing {
  price: string;                // Formatted price
  originalPrice?: string;       // Before discount
  hasDiscount: boolean;
  discountPercentage?: number;
  isSubscription: boolean;
  subscriptionInterval?: string; // "month", "year", etc.
  isPWYW: boolean;              // Pay what you want
  priceRange?: {                // For variants
    min: string;
    max: string;
  };
}

SSR Support

For server-side rendering or static generation, use InitialDataProvider:
// @validate
import { InitialDataProvider } from '@shoppex/ui';
import type { InitialData } from '@shoppex/ui';

// Server-side
async function fetchStoreData(): Promise<InitialData> {
  return { store: null, products: [], groups: [] };
}
const initialData = await fetchStoreData();

// Client
function App() {
  function Storefront() {
    return null;
  }

  return (
    <InitialDataProvider data={initialData}>
      <Storefront />
    </InitialDataProvider>
  );
}
This prevents hydration mismatches and provides instant initial render.