# Salla SDK and events (https://docs.salla.dev/twilight/react/guide/salla-sdk-and-events)



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

[Loaders](https://docs.salla.dev/twilight/react/guide/loading-data) 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 [#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:

```tsx
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.

<Callout>
  Check for a hook first. `useWishlist` and `useProduct` wrap the common flows and keep the page in sync.
  See [Hooks and slots](https://docs.salla.dev/twilight/react/guide/hooks-and-slots) and the [wishlist hook](https://docs.salla.dev/twilight/react/hooks/use-wishlist).
</Callout>

## Stay SSR-safe [#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:

```tsx title="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 [#react-to-events]

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

| Event              | When                                                         |
| ------------------ | ------------------------------------------------------------ |
| `cart::updated`    | The cart changed. The payload has a `count`.                 |
| `cart::item.added` | An item was added.                                           |
| `route::changed`   | The engine finished a navigation in the browser. See below.  |
| `Product Viewed`   | The engine opened a product page, once per visit. See below. |
| `theme::ready`     | A **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):

```tsx title="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);
  };
}, []);
```

4. **Never call `window.location.reload()`** to refresh data. Update React state from the event instead.

### Know when the page changes [#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.

| Field         | Type                        | Description                                                 |
| ------------- | --------------------------- | ----------------------------------------------------------- |
| `slug`        | `string`                    | The kind of page, such as `product.single` or `cart`.       |
| `title`       | `string`                    | The page title.                                             |
| `id`          | `string \| number`          | The product, category or similar id, when the page has one. |
| `url`         | `string`                    | The page's address.                                         |
| `parent`      | `{ id, name, url } \| null` | The 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. App and snippet code that belongs to one kind of page should listen for the SDK's `page::view` instead, which also covers the first view: see [Run app code on a page](https://docs.salla.dev/twilight/react/recipes/run-app-code-on-a-page).

### Know when a product is viewed [#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:

```tsx
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](https://docs.salla.dev/twilight/react/recipes/react-to-cart-events), and for analytics [Track page views](https://docs.salla.dev/twilight/react/recipes/track-page-views).

## Where to go next [#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](https://docs.salla.dev/twilight/react/recipes/add-a-page), or browse the other [recipes](https://docs.salla.dev/twilight/react/recipes).
