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

React to cart changes

Beginner8 min

Listen to the SDK cart events in an effect, and remove each listener with the same function you added.

Goal: a part of your theme (a cart counter, a "just added" highlight, a free-shipping bar) updates the moment the shopper's cart changes, wherever the change came from.

Mechanism: the cart lives in the browser, in the Salla SDK (window.Salla). Whenever it changes the cart, the SDK announces it with an event: cart::updated with the new cart, cart::item.added with the product id, cart::item.added.failed, and more. A component listens with Salla.event.on(name, listener) inside useEffect, and stops listening in the effect's cleanup with Salla.event.off(name, listener), handing it the very same function.

1. Try it

Runs in the browser…

2. Listen in an effect, clean up with the same function

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

/** How many items are in the shopper's cart, kept current by the SDK's cart events. */
export function CartBadge() {
  const [count, setCount] = useState<number | null>(null);
  const [justAdded, setJustAdded] = useState(false);

  useEffect(() => {
    const salla = getSallaSDK();
    if (!salla?.event) return; // effects never run on the server; the SDK may still be missing

    // Named functions, created once per mount: off() removes a listener only
    // when it is handed the very same function that on() received.
    const onUpdated = (...args: unknown[]) => {
      const cart = args[0] as { count?: number } | undefined;
      if (typeof cart?.count === 'number') setCount(cart.count);
    };
    const onItemAdded = () => setJustAdded(true);

    salla.event.on('cart::updated', onUpdated);
    salla.event.on('cart::item.added', onItemAdded);
    return () => {
      salla.event.off('cart::updated', onUpdated);
      salla.event.off('cart::item.added', onItemAdded);
    };
  }, []);

  // Let the highlight fade after a moment.
  useEffect(() => {
    if (!justAdded) return;
    const timer = window.setTimeout(() => setJustAdded(false), 1500);
    return () => window.clearTimeout(timer);
  }, [justAdded]);

  if (count === null) return null;
  return (
    <span
      className={`inline-flex min-w-6 justify-center rounded-full px-2 text-sm text-white ${
        justAdded ? 'bg-green-600' : 'bg-primary'
      }`}
    >
      {count}
    </span>
  );
}
  • useEffect runs only in the browser, after the page appears, so window.Salla is safe there. On the server there is no SDK.
  • The effect's dependency list is []: it subscribes once, when the component appears, and the cleanup runs once, when it disappears.
  • cart::updated carries the cart itself (count, total, sub_total…); cart::item.added carries the SDK's response and then the product id.

The cart helpers work too, as long as each on helper is undone by its own off helper:

The helper style (inside the same effect)
// The same listener through the cart helpers. Remove it with the matching off helper
// and the same function: `on` stores a wrapper, so `event.off('cart::updated', onUpdated)`
// would find nothing to remove. getSallaSDK() is typed; the bare global is not.
const events = getSallaSDK()?.cart.event;
events?.onUpdated?.(onUpdated);
return () => events?.offUpdated?.(onUpdated);

3. Mount it once

app/hooks/index.tsx (excerpt)
import { hookRegistry, HookName } from '@salla.sa/twilight-theme-engine/hooks';
import { CartBadge } from '../components/cart/CartBadge';

// Inside registerThemeHooks(): mounted once, on every page, for as long as the page is open.
hookRegistry.register(HookName.HEADER_END, () => <CartBadge />);
In engine terms

Salla.event is an EventEmitter2 created with wildcard: true and :: as delimiter. Its off(event, fn) removes a listener only when listener === fn (or listener.listener/_origin === fn), and throws "removeListener only takes instances of Function" without one. The cart helpers come from the SDK's BaseEvent: onUpdated(fn) subscribes an async wrapper it remembers per function (a Map in 2.14.583, fn.asyncWrapper in 3.0.0-beta.1) and offUpdated(fn) removes that wrapper. itemAdded(response, productId) first calls updated(response.data), which stores cart.summary and dispatches cart::updated with the cart, then dispatches cart::item.added. The SDK loads from Salla's CDN at the version the store pins; the one pinned by the demo store was read for this page.

4. See the leak the wrong cleanup causes

Two listener components that differ only in their cleanup. The count comes from the SDK's own emitter.

Runs in the browser…

Traps

Go deeper: getSallaSDK, the lesson Changing data in the browser: the Salla SDK, the concept Reading and writing data, and Add content to a slot for mounting a component on every page.