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.Copy
// @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
Copy
// @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.Copy
// @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).Copy
// @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.Copy
// @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.Copy
// @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.Copy
// @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.Copy
// @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.Copy
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
Copy
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:
Copy
// @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>
);
}