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, useInitialDataProvider:
// @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>
);
}