--- 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.