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:
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:
| 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:
- Always unsubscribe. Subscribe in
useEffectand calloffin its cleanup, with the same function. Otherwise each mount adds another listener. - Use one emitter for on and off. Subscribe by event name on
salla.event, and unsubscribe there too. The helpers likesalla.cart.event.onUpdatedhave their own matchingoffUpdated. - The SDK may not be ready yet. If
window.salla.eventis missing, listen fortheme::readyondocument, then subscribe. The starter's add-to-cart toast does this (shortened):
// 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);
};
}, []);- 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.
| 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.
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.