Skip to main content

Hooks (Primitives)

Headless hooks that manage state and integrate with @shoppex/sdk. Use these to build custom components or access SDK functionality in your own code.

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, productId?) => 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);
    if (result.valid) {
      console.log(`${result.discount}% off!`);
    }
  };

  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.