---
url: https://docs.youcan.shop/themes/best-practices/events-and-actions.md
---
# Events and actions
A storefront theme has three separate channels for behaviour: actions that change store state, events that let parts of the theme react to each other, and pixel events that report what the customer did. Keep them distinct.
## Actions
### Prefer forms where they work
The [`{% form %}`](/themes/tags/form) tag posts to the storefront and reloads the page. It needs no JavaScript, which means it still works while scripts are loading or if they fail. Use it for add-to-cart on a standard product page.
```liquid
{% form 'product' %}
{% endform %}
```
### Use the SDK for interactive flows
When a reload would break the experience, such as a cart drawer or a variant picker that updates in place, use the [YouCan JS SDK](/themes/sdk/overview):
```js
await youcanjs.cart.addItem({
productVariantId: variantId,
quantity: 1,
});
```
The SDK covers cart (`addItem`, `updateItem`, `removeItem`), checkout (`applyCoupon`, `removeCoupons`, `placeExpressCheckoutOrder`), products (`fetchReviews`, `submitReview`), upsells (`answer`), and location lookups (`getStoreMarketCountries`, `getCountryRegions`, `getCountryCities`).
Pick one approach per theme. Mixing a form post and an SDK call for the same action produces two different states to keep in sync.
### Handle failure
Every SDK call talks to the network and can fail. Catch it and tell the customer, rather than leaving a button spinning:
```js
try {
await youcanjs.cart.addItem({ productVariantId, quantity });
} catch (error) {
toast.show(window.errorStrings.cart, 'error');
}
```
## Theme events
Sections don't know about each other. When adding to the cart should update a badge in the header, a drawer, and a subtotal in a third section, a small publish and subscribe module is cleaner than having each component reach into the others.
```js
const subscribers = {};
function subscribe(eventName, callback) {
subscribers[eventName] = [...(subscribers[eventName] ?? []), callback];
return function unsubscribe() {
subscribers[eventName] = subscribers[eventName].filter(cb => cb !== callback);
};
}
function publish(eventName, data) {
subscribers[eventName]?.forEach(callback => callback(data));
}
```
Name events in one place so a typo can't silently unsubscribe a component:
```js
const PUB_SUB_EVENTS = {
cartUpdate: 'cart/update',
cartError: 'cart/error',
couponUpdate: 'coupon/update',
};
```
Publish after the action succeeds, and pass the new state rather than making subscribers refetch it:
```js
const cart = await youcanjs.cart.addItem({ productVariantId, quantity });
publish(PUB_SUB_EVENTS.cartUpdate, { cart, source: 'product-form' });
```
Include a `source` so a component can ignore the event it caused itself.
Keep the unsubscribe function and call it when a component is torn down. Sections are removed and re-rendered while a seller is customizing, and subscribers that outlive their elements leak and throw.
## Pixel events
Storefront analytics and marketing pixels listen on a separate channel, published through `window.Dotshop.pixels`:
```js
window.Dotshop.pixels.publish('view-content', product);
window.Dotshop.pixels.publish('add-to-cart', selectedVariant);
window.Dotshop.pixels.publish('initiate-checkout', cart);
```
| Event | Publish when |
| ----- | ------------ |
| `view-content` | A product page renders. |
| `add-to-cart` | An item is successfully added to the cart. |
| `initiate-checkout` | The customer moves from the cart to checkout. |
Publish these once, at the point the thing actually happened, and only after it succeeded. Firing `add-to-cart` on the click rather than on the response inflates every seller's conversion data.
Don't route theme state through the pixel channel, and don't route analytics through your own pub/sub. They have different consumers and different lifetimes.