Every public hook in @numueg/theme-sdk, grouped by surface. Each entry: signature, return shape, when to use, gotchas.Shop + theme#
useShop()#
function useShop(): {
id: string;
name: string;
subdomain: string;
domain: string;
currency: string;
default_language: "en" | "ar";
logo_url: string | null;
formatUrl: (path: string) => string; // /products/x → https://<sub>.numueg.app/products/x
};
The current store. Always available — <NuMuProvider> populates it from ctx.store.function Header() {
const shop = useShop();
return (
<header className="flex justify-between p-4">
{shop.logo_url ? (
<img src={shop.logo_url} alt={shop.name} className="h-10" />
) : (
<span className="font-bold text-xl">{shop.name}</span>
)}
<a href={shop.formatUrl("/cart")}>Cart</a>
</header>
);
}
// Subdomain-aware canonical URL for SEO
function CanonicalLink({ path }: { path: string }) {
const shop = useShop();
return <link rel="canonical" href={shop.formatUrl(path)} />;
}
useThemeSettings()#
function useThemeSettings(): ThemeSettingsV3;
Resolved customizer state (theme-level + section-level). For section-level values, prefer useSection() — it's scoped.useLocalization() / useDirection() / useTranslation()#
function useLocalization(): { locale: "en" | "ar"; supportedLocales: string[] };
function useDirection(): "ltr" | "rtl";
function useTranslation(): (key: string, params?: Record<string, string>) => string;
const t = useTranslation();
return <button>{t("product.addToCart")}</button>;
Keys come from your locales/<lang>.json. Falls back to the default locale, then to the key string.useCurrency()#
function useCurrency(): {
currency: string;
rate: number; // multiplier vs store currency
symbol: string;
format: (cents: number) => string;
};
Phase 6 multi-currency presentment. Default is the store currency at rate 1.0.Page#
usePage()#
function usePage(): {
type: string; // "home" | "product" | "cart" | ...
title?: string;
handle?: string;
data?: Record<string, unknown>;
};
Use a switch on page.type in your main.tsx to route. The discriminated union narrows data per branch.Products#
useProduct() / useProductOptional()#
function useProduct(): Product; // throws if not in <ProductProvider>
function useProductOptional(): Product | null;
Use the Optional variant on cards that might render outside a product context.useProducts(opts?)#
function useProducts(opts?: {
limit?: number;
page?: number;
category_id?: string;
search?: string;
}): {
items: Product[];
total: number;
loading: boolean;
error: Error | null;
};
Calls /api/storefront/products. Server-side cached for 60s.useVariantSelection(product)#
Phase 8.1 — PDP variant picker state.function useVariantSelection(
product: Pick<Product, "options" | "variants">,
opts?: { autoSelect?: boolean }
): {
selection: Record<string, string>;
variant: ProductVariant | null;
select: (axis: string, value: string) => void;
reset: () => void;
availability: Record<string, Set<string>>;
isComplete: boolean;
};
Auto-selects the default in-stock variant on mount (Shopify default). availability lets you grey out swatches whose paired variants are all sold out for the current locked axes.const { selection, variant, select, availability, isComplete } = useVariantSelection(product);
return (
<>
{product.options.map(axis => (
<div key={axis.name}>
<label>{axis.name}: {selection[axis.name]}</label>
{(axis.values || []).map(v => (
<button
key={v}
disabled={!availability[axis.name].has(v)}
onClick={() => select(axis.name, v)}
>{v}</button>
))}
</div>
))}
<button disabled={!isComplete || !variant?.is_in_stock}>
{variant ? `Add — ${variant.price}` : "Choose options"}
</button>
</>
);
useRelatedProducts(productId, opts?)#
function useRelatedProducts(
productId: string | null | undefined,
opts?: { limit?: number }
): { items: Product[]; loading: boolean; error: Error | null };
Same-category siblings. Returns [] (not error) when the endpoint is missing — themes branch on items.length to render the section or skip.Collections#
useCollection() / useCollectionOptional() / useCollections()#
Same shape as the product hooks. useCollections() lists all categories.Cart#
useCart()#
function useCart(): {
cart: Cart;
loading: boolean;
addItem: (productId: string, variantId?: string | null, quantity?: number) => Promise<void>;
removeItem: (itemId: string) => Promise<void>;
updateQuantity: (itemId: string, quantity: number) => Promise<void>;
applyDiscount: (code: string) => Promise<void>;
removeDiscount: () => Promise<void>;
updateNote: (note: string) => Promise<void>;
clearCart: () => Promise<void>;
};
Mutations call /api/cart/* proxies. They auto-refresh cart on success and dispatch a numu:cart:updated window event for non-React listeners (header cart badge etc.).Idempotency-key headers are stamped automatically — double-clicks won't double-add.Customer#
useCustomer()#
function useCustomer(): Customer | null;
null for guests. <NuMuProvider> reads customer_access_token from cookies on mount.useCustomerActions()#
function useCustomerActions(): {
login: (email: string, password: string) => Promise<...>;
register: (data: RegisterData) => Promise<...>;
logout: () => Promise<void>;
requestRecover: (email: string) => Promise<...>;
confirmReset: (token: string, password: string) => Promise<...>;
updateProfile: (patch: Partial<Customer>) => Promise<...>;
changePassword: (currentPassword: string, newPassword: string) => Promise<...>;
};
All return discriminated { ok: true, data } | { ok: false, error } so themes can render field-level errors without try/catch.useCustomerAddresses()#
function useCustomerAddresses(): {
addresses: Address[];
loading: boolean;
addAddress: (a: Omit<Address, "id">) => Promise<Address>;
updateAddress: (id: string, patch: Partial<Address>) => Promise<Address>;
deleteAddress: (id: string) => Promise<void>;
setDefaultAddress: (id: string) => Promise<void>;
};
useOrders(opts?) / useOrder(id) / useReorder()#
function useOrders(opts?: { page?: number; limit?: number }): { items: Order[]; total: number; loading: boolean; };
function useOrder(id: string): { order: Order | null; loading: boolean; };
function useReorder(): {
result: ReorderResult | null;
loading: boolean;
error: Error | null;
reorder: (orderId: string) => Promise<ReorderResult | null>;
reset: () => void;
};
ReorderResult.skipped[] includes per-line reasons (product_deleted, out_of_stock, etc.) — themes show them in a banner.Checkout#
useCheckout()#
Phase 7 programmatic driver. Drive the full multi-step flow without leaving the theme's own UI.const checkout = useCheckout();
await checkout.contact.set({ email, phone, shipping_address });
const rates = await checkout.shipping.refresh();
await checkout.shipping.select(rates[0].id);
await checkout.payment.select("paymob_card", { saved_payment_method_id: null });
const { order_id, payment_url } = await checkout.placeOrder();
if (payment_url) window.location.assign(payment_url);
Reads/writes the same numu_checkout_state sessionStorage blob the platform's multi-step routes use, so themes can mix-and-match (drive 2 steps programmatically, hand off the rest to the platform UI).useShippingRates()#
function useShippingRates(opts: {
address?: Partial<Address>;
location_id?: string;
}): {
rates: ShippingRateOption[];
loading: boolean;
error: Error | null;
refresh: () => Promise<void>;
};
Each rate carries estimated_days_min/estimated_days_max from ShippingZone. Useful for theme-rendered "Delivery: 2–4 days" labels at the cart level.Gift cards#
useGiftCardBalance()#
function useGiftCardBalance(): {
balance: GiftCardBalance | null;
loading: boolean;
error: Error | null;
check: (code: string) => Promise<GiftCardBalance | null>;
reset: () => void;
};
check() returns null on miss/expired/depleted with error populated. Themes typically wire this on the checkout payment step.Search + nav#
useSearch(query, opts?)#
function useSearch(
query: string,
opts?: { types?: ("product" | "collection" | "page" | "article")[]; predictive?: boolean }
): {
results: SearchResults;
loading: boolean;
};
Hits the Postgres tsvector endpoint. Predictive mode (autocomplete) caps results at 5 per type.useNavigation(handle)#
function useNavigation(handle: string): { items: NavItem[]; loading: boolean };
Resolves a merchant-defined nav menu by handle (e.g. "main-menu", "footer"). Configured in the hub under Online Store → Navigation.Analytics + apps + wishlist#
useAnalytics()#
function useAnalytics(): {
track: (event: string, payload?: Record<string, unknown>) => void;
};
Fans out to merchant-configured pixels (GA4, Meta CAPI, TikTok). Theme code only emits canonical events — view_item, add_to_cart, begin_checkout, purchase, etc.useApp(slug)#
Phase 9 platform stub. Returns null when the app isn't installed; otherwise gives access to the app's manifest + data.function useApp(slug: string): {
available: boolean;
data: Record<string, unknown> | null;
setData: (key: string, value: unknown) => Promise<void>;
getData: (key: string) => Promise<unknown>;
};
Always branch on .available so themes degrade gracefully when the app isn't installed. Lint rule use-app-no-availability-check enforces this.useWishlist()#
function useWishlist(): {
items: WishlistItem[];
has: (productId: string, variantId?: string | null) => boolean;
addToWishlist: (productId: string, variantId?: string | null) => void;
removeFromWishlist: (productId: string, variantId?: string | null) => void;
};
Guest wishlists persist in localStorage; merge into the customer's server-side wishlist on login.Sections + blocks#
useSection() / useSectionOptional()#
function useSection(): {
id: string;
type: string;
settings: Record<string, unknown>;
blocks: Record<string, BlockInstance>;
block_order: string[];
};
Inside a <Section> boundary, returns the resolved section instance. Use useSectionOptional() for components that might render outside one.Helpers#
useImage()#
function useImage(): {
src: (url: string, opts?: { w?: number; h?: number; q?: number }) => string;
srcset: (url: string, widths: number[]) => string;
};
Builds srcset strings for the platform's image-transform service.useMoney()#
function useMoney(): {
format: (cents: number, currency?: string) => string;
formatCurrency: (currency: string) => string;
};
Locale + RTL-aware formatter. Prefer <Money cents={123}> for inline usage. Modified at 2026-09-19 15:52:25