React to cart changes
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
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>
);
}
useEffectruns only in the browser, after the page appears, sowindow.Sallais 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::updatedcarries the cart itself (count,total,sub_total…);cart::item.addedcarries 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 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
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.