Salla SDK and events

Change cart and wishlist data in the browser with the Salla SDK, and react to its events safely.

Read this when a click should change store data, such as adding to the cart or the wishlist.

Loaders give you the page's data on the server. For things customers change by clicking, you use the Salla SDK: a script loaded in the browser that talks to the store. In code it is the global window.salla. In prose we call it Salla.

Call the SDK

Cart and wishlist actions are plain calls. In your component code, read the SDK from window.salla, and only after the page is in the browser:

async function addToCart(id: number) {
  await window.salla?.cart.addItem(id, 1);
}

async function toggleWishlist(id: number) {
  await window.salla?.wishlist.toggle(id);
}

Click handlers run in the browser, so they are a safe place for SDK calls.

Check for a hook first. useWishlist and useProduct wrap the common flows and keep the page in sync. See Hooks and slots and the wishlist hook.

Stay SSR-safe

The server has no window. If you touch window.salla while rendering, the server build crashes or the page shows different content on the server and in the browser (a hydration mismatch). So:

  • Use the SDK only inside event handlers or useEffect.
  • Start state from a neutral value (like 0), then fill it in an effect.

The starter theme's mobile bottom bar does exactly this for the cart badge. It starts at 0 and reads the SDK in an effect:

app/components/layout/MobileBottomBar.tsx
import { useEffect, useState } from 'react';

function useCartCount(): number {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const onUpdated = (...args: unknown[]) => {
      const summary = args[0] as { count?: number | string | null } | undefined;
      setCount(Number(summary?.count) || 0);
    };
    const sdk = window.salla;
    sdk?.event?.on?.('cart::updated', onUpdated);
    return () => sdk?.event?.off?.('cart::updated', onUpdated);
  }, []);

  return count;
}

(Shortened. The real one also reads the saved count at start.)

React to events

The SDK announces changes as named events. Subscribe with salla.event.on(name, handler). Some you will meet:

EventWhen
cart::updatedThe cart changed. The payload has a count.
cart::item.addedAn item was added.
route::changedThe engine finished a navigation in the browser. See below.
Product ViewedThe engine opened a product page, once per visit. See below.
theme::readyA document event: the SDK finished starting.

Follow these rules and you avoid most bugs:

  1. Always unsubscribe. Subscribe in useEffect and call off in its cleanup, with the same function. Otherwise each mount adds another listener.
  2. Use one emitter for on and off. Subscribe by event name on salla.event, and unsubscribe there too. The helpers like salla.cart.event.onUpdated have their own matching offUpdated.
  3. The SDK may not be ready yet. If window.salla.event is missing, listen for theme::ready on document, then subscribe. The starter's add-to-cart toast does this (shortened):
app/components/cart/AddProductToast.tsx
// inside the component
useEffect(() => {
  const onItemAdded = (...args: unknown[]) => {
    // show the toast
  };

  let bound = false;
  const bind = () => {
    const event = window.salla?.event;
    if (bound || !event?.on) return;
    bound = true;
    event.on('cart::item.added', onItemAdded);
  };

  bind();
  document.addEventListener('theme::ready', bind);

  return () => {
    document.removeEventListener('theme::ready', bind);
    window.salla?.event?.off?.('cart::item.added', onItemAdded);
  };
}, []);
  1. Never call window.location.reload() to refresh data. Update React state from the event instead.

Know when the page changes

After every navigation in the browser, the engine dispatches route::changed with the current page. Use it for analytics or anything that must run on each page, because a script that runs once misses later navigations.

FieldTypeDescription
slugstringThe kind of page, such as product.single or cart.
titlestringThe page title.
idstring | numberThe product, category or similar id, when the page has one.
urlstringThe page's address.
parent{ id, name, url } | nullThe parent, such as a parent category.
breadcrumbs{ name, url }[]The breadcrumb trail.

Pages without page data, such as a 404, send an empty object, so check slug before using the payload. The event covers navigations after the first load; for the first view, send it when your component mounts.

Know when a product is viewed

On a product page the engine also dispatches Product Viewed, once per visit, the first load included. It waits until the SDK is ready and sends the payload Twig's product page sends, so the store's analytics, pixels and apps that remember viewed products work without code from you. Do not dispatch it again yourself, or each view counts twice.

The handler receives an array whose first item is the product: id, name, price, currency (the customer's chosen one), category, brand, sku, image, url and a few more. Subscribe the way rule 3 above shows, with the same cleanup:

const onViewed = (...args: unknown[]) => {
  const [product] = args[0] as Array<{ id: number; name: string }>;
  // send product.id to your own tool
};
// in bind(): event.on('Product Viewed', onViewed);
// in the cleanup: window.salla?.event?.off?.('Product Viewed', onViewed);

It fires only when the page's loader returns a page whose slug is product.single together with the product, as the engine's product route does.

Step by step: React to cart events, and for analytics Track page views.

Where to go next

That completes the guide: routes, data, layout, slots, styling, language, settings, head and the SDK.

Next: put it together with the Add a page recipe, or browse the other recipes.

On this page