Skip to content
Twilight React Playground
ثيم رائدaren

Changing data in the browser: the Salla SDK

Beginner12 min

Add to cart and other actions go through window.Salla in the browser; listen to its events and clean up after yourself.

Loaders and queries read data. Changing it (adding to the cart, saving to the wishlist, applying a coupon, logging in) happens in the shopper's browser, through the Salla SDK: a script Salla loads on every storefront page and exposes as window.Salla.

The SDK does two jobs. Its methods send the change to Salla (Salla.cart.addItem(id)), and its events announce what happened (cart::item.added, cart::updated) to anything that listens: your components, Salla's own web components, installed apps.

This part is already plain JavaScript
// Plain JavaScript, in the browser, on any Salla storefront.
await Salla.onReady();

Salla.cart.event.onItemAdded((response, productId) => {
  console.log('added', productId, 'cart now has', response.data.cart.count, 'items');
});

await Salla.cart.addItem(1303461379);

Add to your guest cart, watch the events

Pick a product and add it. The request goes to the demo store for your own visitor session, and the log shows each event as the SDK fires it. Then untick Listening and add again: the cart still changes, but nothing is logged, because the component removed its listeners.

Real requests: your own guest cart on the demo storeSDK: starting… · items in cart: ?
No events yet. Add a product.

A product marked needs options (a size, a color) cannot be added with its id alone: try one and read the log. Product pages use the engine's AddToCartForm, which sends the options the shopper chose.

Listening from a component

A React component listens in useEffect, because the SDK exists only in the browser, and returns a cleanup function that removes the listener. Without it, every time the component appears it adds one more listener, and old ones keep running after it is gone.

app/components/CartCount.tsx
import { useEffect, useState } from 'react';
import { getSallaSDK } from '@salla.sa/twilight-theme-engine/utils';

export function CartCount() {
  const [count, setCount] = useState<number | null>(null);

  useEffect(() => {
    const events = getSallaSDK()?.cart.event; // browser only: there is no SDK on the server
    if (!events) return;

    const onUpdated = (...args: unknown[]) => {
      const summary = args[0] as { count?: number };
      setCount(summary.count ?? null);
    };

    events.onUpdated?.(onUpdated);
    return () => events.offUpdated?.(onUpdated); // the same function, or it is never removed
  }, []);

  return count === null ? null : <span className="cart-count">{count}</span>;
}

Calling a method needs no effect: an event handler only runs after a click, and a click only happens in the browser.

app/components/QuickAddButton.tsx
import { useState } from 'react';
import { getSallaSDK } from '@salla.sa/twilight-theme-engine/utils';

export function QuickAddButton({ productId }: { productId: number }) {
  const [busy, setBusy] = useState(false);

  return (
    <button
      type="button"
      disabled={busy}
      onClick={async () => {
        setBusy(true); // a click only happens in the browser, so the SDK is there
        try {
          await getSallaSDK()?.cart.addItem(productId);
        } catch {
          // cart::item.added.failed has fired too: show a message if you need one
        } finally {
          setBusy(false);
        }
      }}
    >
      Add to cart
    </button>
  );
}
In engine terms
  • The SDK sits on window under two names, Salla and salla, and they are the same object. The engine's published types declare only the lower-case one (salla?: SallaSDK, src/types/salla-sdk.ts), so TypeScript follows window.salla and rejects window.Salla; reach it through getSallaSDK() from @salla.sa/twilight-theme-engine/utils, which returns it, or undefined on the server, and is typed either way. TwilightProvider calls Salla.init() and awaits Salla.onReady() before it dispatches theme::ready on document.
  • In the SDK each cart event helper is generated: onItemAdded(fn) stores fn.asyncWrapper and listens with it; offItemAdded(fn) removes fn.asyncWrapper ?? fn. Event names are cart::<name>: updated, item.added, item.added.failed, item.updated, item.deleted, coupon.added….
  • itemAdded first dispatches updated with the new cart summary ({ id, count, total, sub_total, … }), then item.added with (response, productId). The summary is also stored under Salla.storage.get('cart.summary').
  • Salla.cart.addItem(id) with a bare id uses the quick-add endpoint; an object with a quantity, such as { id, quantity: 2 }, uses the full add-item endpoint.
  • Nothing in the engine re-reads a cart query after the SDK changes the cart: refetch on onUpdated yourself, as the CartSummary demo does.
  • Engine hooks built on the SDK: useWishlist, useCoupon, useBlogLike; components: AddToCartForm. Reference for the accessor: getSallaSDK.
Check yourself

Why does the effect in CartCount return a function?