Skip to main content

React (Reference Theme)

Our reference themes are built with Vite + React:
  • themes/default
  • themes/classic
This page is not “React-only”. It is just the easiest place to see a full working theme.

Component Tree

The reference theme follows this hierarchy:
App
 └─ ThemeSettingsProvider        // merged theme settings
     └─ ThemeStyleApplier        // applies settings as CSS variables
     └─ InitialDataProvider      // pre-rendered / injected data
         └─ ToastProvider
             └─ BrowserRouter
                 └─ AppShell
                     ├─ Header
                     ├─ Routes
                     │   ├─ Home       (/)
                     │   ├─ Product    (/product/:slug)
                     │   ├─ Cart       (/cart)
                     │   ├─ Checkout   (/checkout)
                     │   ├─ Page       (/page/:slug)
                     │   └─ Terms      (/terms)
                     └─ Footer
LayerPurpose
ThemeSettingsProviderFetches and merges theme settings, provides them via context
ThemeStyleApplierReads resolved settings via useThemeSettings() and calls applyThemeSettingsToCss()
InitialDataProviderPasses injected initial data (window.__SHOPPEX_INITIAL__) down the tree
ToastProviderNotification system
BrowserRouterReact Router for client-side navigation
AppShellLayout shell with Header, Routes, and Footer
ThemeSettingsProvider wraps InitialDataProvider — settings are available everywhere, including in the data layer.

Where SDK Init Happens

  • HTML loads the SDK via CDN: themes/default/index.html
  • The app bootstraps + calls shoppex.init(...): themes/default/src/main.tsx
The bootstrap sequence:
1

Load SDK via CDN script tag

The SDK is available as window.shoppex before React mounts.
2

Resolve the store slug

From subdomain (*.myshoppex.io), env var (VITE_SHOP_SLUG), or custom domain resolution via shoppex.resolveStoreByDomain().
3

Call shoppex.init(slug, options)

Initializes the SDK with the resolved slug and API base URL.
4

Check for initial data

Reads window.__SHOPPEX_INITIAL__ (injected during build) and passes it to App as a prop.
5

Render or hydrate

If SSR content exists and initial data is present, hydrate with hydrateRoot. Otherwise, create a fresh root with createRoot.

Where Data Loading Happens: useStore()

The useStore() hook is the single place where storefront data is resolved. It follows a two-tier strategy:
  1. Use InitialDataContext for first paint — if the page was pre-rendered (or the platform injected window.__SHOPPEX_INITIAL__), the data is available immediately.
  2. Revalidate via the SDK — even if initial data exists, your theme should still call shoppex.getStorefront() after shoppex.init() to protect against stale HTML (for example right after a migration/import). You can do this in the background and update state once the fresh response arrives.
// In your theme, implement a `useStore()` hook that returns storefront data
// (use `window.__SHOPPEX_INITIAL__` for fast first paint, then revalidate via `shoppex.getStorefront()`).
declare function useStore(): {
  store: { name: string } | null;
  products: Array<{ uniqid: string; title: string }>;
  groups: any[];
  isLoading: boolean;
  error: string | null;
};

function ProductList() {
  const { store, products, groups, isLoading, error } = useStore();

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <div>
      <h1>{store?.name}</h1>
      {products.map((p) => (
        <div key={p.uniqid}>{p.title}</div>
      ))}
    </div>
  );
}

Return Value

FieldTypeDescription
storeShop | nullStore metadata (name, logo, settings)
productsProduct[]All products
groupsProductGroup[]Product groups/collections
isLoadingbooleantrue while data is being fetched
errorstring | nullError message if fetch failed
The fetched storefront data is cached at module scope. Subsequent useStore() calls in other components reuse the same data without re-fetching.

Cart UI: useCartSync() and emitCartChanged()

useCartSync and emitCartChanged are theme-level patterns, not SDK features. The SDK manages cart data; these helpers keep the UI in sync.

CartSnapshot Interface

useCartSync() returns a CartSnapshot — a reactive view of the current cart:
FieldTypeDescription
itemsCartItem[]Current cart items
itemCountnumberNumber of distinct line items
totalQuantitynumberSum of all item quantities
totalPricenumberComputed total price
totalPriceIsEstimatebooleanWhether the price is an estimate (e.g. missing price data)

emitCartChanged()

After any cart mutation, dispatch a custom event so all listeners re-read the cart:
export function emitCartChanged(): void {
  window.dispatchEvent(new CustomEvent('shoppex:cart-changed'));
}

useCartSync()

The hook listens for the custom event and storage events (for cross-tab sync) and returns the current cart snapshot:
export function useCartSync(): CartSnapshot {
  const [snapshot, setSnapshot] = useState<CartSnapshot>(getSnapshot());

  useEffect(() => {
    const update = () => setSnapshot(getSnapshot());
    window.addEventListener('shoppex:cart-changed', update);
    window.addEventListener('storage', update);
    return () => {
      window.removeEventListener('shoppex:cart-changed', update);
      window.removeEventListener('storage', update);
    };
  }, []);

  return snapshot;
}
The getSnapshot() function calls shoppex.getCart() and shoppex.getCartStats() to build the snapshot.

Usage Pattern

// assumes: useCartSync and emitCartChanged from your theme's hooks (see above)
declare function useCartSync(): { items: any[]; totalQuantity: number; subtotal: number };
declare function emitCartChanged(): void;

function AddToCartButton({ productId, variantId }: { productId: string; variantId: string }) {
  const handleClick = () => {
    shoppex.addToCart(productId, variantId);
    emitCartChanged(); // notify all listeners
  };
  return <button onClick={handleClick}>Add to Cart</button>;
}

function CartBadge() {
  const { totalQuantity } = useCartSync();
  return <span>{totalQuantity > 0 ? totalQuantity : null}</span>;
}
This pattern applies to all cart operations: addToCart, updateCartItem, removeFromCart, and clearCart. Always call emitCartChanged() after the SDK method.

Theme Settings: useThemeSettings() and applyThemeSettingsToCss()

ThemeSettingsProvider

The provider does three things at startup:
1

Resolve defaults from theme.config.ts

Reads the settings schema and extracts all default values into a ResolvedThemeSettings object.
2

Fetch published overrides

If the SDK is initialized, calls shoppex.fetchPublishedThemeSettings(shopSlug) to get merchant-configured values.
3

Merge defaults + overrides

Published values are merged on top of defaults. Missing keys keep their default values.

useThemeSettings()

Returns the fully resolved settings from context (defaults merged with published overrides):
import { useThemeSettings } from '@/context/ThemeSettingsContext';

function BrandedSection() {
  const settings = useThemeSettings();
  const primaryColor = (settings.colors as Record<string, string>)?.primary;

  return (
    <section style={{ borderColor: primaryColor }}>
      Branded content
    </section>
  );
}
useThemeSettings() must be called inside ThemeSettingsProvider. It throws if used outside the provider tree.

applyThemeSettingsToCss()

Maps resolved theme settings to CSS custom properties on document.documentElement. Called automatically by ThemeStyleApplier whenever settings change.
Settings PathCSS VariableDescription
colors.background--surfaceBackground color
colors.surface--card-bgCard/surface color
colors.primary--brand-600Primary brand color
colors.primaryDark--brand-700Darker brand variant (fallback: colors.secondary, then darken primary 10%)
colors.text--textMain text color
colors.textMuted--text-mutedMuted text color
colors.textContrast--text-contrastContrast text color
colors.border--borderBorder color
colors.muted--mutedMuted background
colors.accent--accentAccent color
colors.hover--hoverHover state color
colors.success--successSuccess color
colors.error--destructiveError/destructive color
typography.fontFamily--font-family-bodyBody font family
typography.headingFont--font-family-headingHeading font (falls back to body)
typography.fontSize--font-size-baseBase font size in px
effects.borderRadius--radiusGlobal border radius in rem
buttons.borderRadius--button-radiusButton-specific radius in px
inputs.height--input-heightInput height in px
inputs.borderRadius--input-border-radiusInput-specific radius in px
header.height--header-heightHeader height in px
Your CSS then references these variables:
body {
  font-family: var(--font-family-body);
  color: rgb(var(--text));
  background: rgb(var(--surface));
}

.btn-primary {
  background: rgb(var(--brand-600));
  border-radius: var(--button-radius);
}
Color values are stored as space-separated RGB triplets (e.g. 124 58 237), not hex. Use rgb(var(--brand-600)) in CSS.

SSR / SSG

If you need SSR/SSG, see:

Next Steps