Configure actions
Every Liquid storefront supports actions out of the box, and every action ships with a default implementation. An app can call updateCart on your theme and it works, even if you've never touched any of this.
That default falls back to a full page reload when it doesn't recognize how your theme renders its cart. You can configure an action to replace the default with your own code, so the same call opens your drawer or re-renders your section instead.
Anchor to What the defaults doWhat the defaults do
Without a configuration, each action runs its default behavior:
updateCart: Writes to the Storefront API, then refreshes the cart in place on a theme it recognizes. If it recognizes none, then it falls back to a full page reload.openCart: Opens a cart drawer it finds on the page, and otherwise navigates to/cart.getCart: Reads the current cart from the Storefront API without affecting the page.
getCart isn't configurable. Calling Shopify.actions.getCart.configure(...) is a TypeScript error and a runtime TypeError.
getCart isn't configurable. Calling Shopify.actions.getCart.configure(...) is a TypeScript error and a runtime TypeError.
Anchor to How the defaults recognize a themeHow the defaults recognize a theme
If your theme is based on Horizon or Dawn, then the defaults refresh your cart in place, so a call doesn't reload the page even before you configure anything.
Your theme keeps that behavior after you fork and rename it, because recognition is based on your cart's code rather than the theme's name. updateCart looks for two shapes:
| Pattern | What it looks for | How it refreshes |
|---|---|---|
| Horizon-style | A <cart-icon> with a renderCartBubble() method, and a <cart-items-component> with updateQuantity() | Dispatches a cart:update event on document for those components to pick up |
| Dawn-style | A cart element such as cart-drawer or cart-items whose prototype has getSectionsToRender() | Asks each one which sections it needs, fetches them, and swaps the HTML in |
openCart looks for an open() method, first on <cart-drawer-component> and then on <cart-drawer>. Horizon defines <cart-drawer-component> but keeps open() on a separate drawer element, so it configures openCart rather than relying on the default.
If your theme renders its cart some other way, then the defaults fall back to a full page reload, and configuring an action is what avoids that.
Anchor to Configuring an actionConfiguring an action
You can register a configuration inside a DOMContentLoaded listener. Put it above {{ content_for_header }} in your layout file, so yours runs before any app code:
Only the first configure call on an action takes effect. Later calls return false and change nothing, so putting yours above {{ content_for_header }} makes sure it runs first.
A configuration accepts two options, and they're two ways of updating your UI. If your theme already has components that re-render when they receive standard events, you can point eventTarget at them and let your existing code do the work. If you'd rather run your own code, such as re-rendering a section, you can write a handler. You can use both together.
Anchor to configuration optionsConfiguration options
- Anchor to eventTargetevent
Targetevent Target (meta: UpdateCartEventTargetMeta) => EventTarget | null(meta: UpdateCartEventTargetMeta) => EventTarget | null A function that returns the element that auto-emitted events dispatch from. Required when you configure
, which is the only action that emits events.doesn't accept it. Returningnulldispatches fromdocument.- Anchor to handlerhandlerhandler(defaultHandler, ...args) => Promise<Result>(defaultHandler, ...args) => Promise<Result>
Replaces the action's behavior. Call
to run Shopify's implementation first, or skip it to replace the behavior outright.
UpdateCartEventTargetMeta
Describes the event an `eventTarget` function is being asked to place.
- type
The event the action is about to emit.
'shopify:cart:lines-update' | 'shopify:cart:note-update' | 'shopify:cart:discount-update' | 'shopify:cart:error' - action
Which kind of line change is happening. Only present when `type` is `shopify:cart:lines-update`.
'add' | 'remove' | 'update'
Anchor to eventTargetevent Target
eventTarget is a function that returns the element that auto-emitted events dispatch from. updateCart is the only action that auto-emits events, so it's the only action that requires this.
You point it at the element your components listen on. If those components re-render when they get the events, your cart updates in place.
Shopify still writes to the Storefront API. Any configuration at all turns off the page reload fallback.
The example below sends each event to the component that owns it. Note changes go to the cart note, discount changes to the discount form, adds to the product form the buyer used, and anything else to the cart:
That routing is possible because the function receives a meta object describing the event it's about to emit:
Anchor to metameta
- Anchor to typetypetype'shopify:cart:lines-update' | 'shopify:cart:note-update' | 'shopify:cart:discount-update' | 'shopify:cart:error''shopify:cart:lines-update' | 'shopify:cart:note-update' | 'shopify:cart:discount-update' | 'shopify:cart:error'requiredrequired
The event the action is about to emit.
- Anchor to actionactionaction'add' | 'remove' | 'update''add' | 'remove' | 'update'
Which kind of line change is happening. Only present when
typeisshopify:cart:lines-update.
If the function returns null, then the event dispatches from document instead.
Anchor to handlerhandler
handler is an optional async function that runs in place of the default handler.
You can write a handler when your storefront updates its UI in code rather than through event listeners. A handler also covers work a re-render can't do, like fetching and swapping section HTML. For openCart it's the only option, because the handler is the entire behavior.
The example below keeps Shopify's cart write by calling defaultHandler() first, then fetches the cart drawer section and swaps in the new HTML. The same listener configures openCart to open that drawer:
A handler receives defaultHandler first, followed by the same arguments the caller passed to the action:
Anchor to handler argumentsHandler arguments
- Anchor to defaultHandlerdefault
Handlerdefault Handler () => Promise<Result>() => Promise<Result>requiredrequired Runs Shopify's implementation and resolves with the result the action would have returned on its own.
- Anchor to payloadpayloadpayloadUpdateCartPayloadUpdateCartPayload
The payload the caller passed.
takes no payload.- Anchor to optionsoptionsoptionsUpdateCartOptionsUpdateCartOptions
The options the caller passed, such as an
.takes no options.
UpdateCartPayload
The payload for an `updateCart` call.
- cartId
The cart to update. If omitted, the action finds the cart from the browser cookie or the AJAX API.
string - lines
The lines to add, update, or remove.
CartLineInput[] - note
A new cart note.
string - discountCodes
The complete set of discount codes the cart should end up with.
string[]
CartLineInput
A cart line to add, update, or remove.
- id
The existing line ID, from either the Storefront API or the AJAX cart API. Include it to update or remove a line, and leave it out to add one.
string - merchandiseId
The product variant GID, or a raw variant ID. Required when adding a line.
string - quantity
The quantity for the line. Set it to `0` to remove the line.
number - attributes
Custom key-value pairs to attach to the line.
Array<{ key: string, value: string }> - sellingPlanId
The selling plan GID, or a raw selling plan ID, for a subscription or other selling plan.
string
UpdateCartOptions
Options for an `updateCart` call.
- signal
Aborts the request when the signal fires.
AbortSignal - event
Shapes the events the call emits.
UpdateCartEventOptions
UpdateCartEventOptions
Shapes the events an `updateCart` call emits.
- context
Where the change came from. Sets the `context` field on the emitted `shopify:cart:lines-update` and `shopify:cart:note-update` events, and defaults to `standard-action`.
'product' | 'cart' | 'dialog' | 'standard-action' - detail
Custom data attached to the emitted events, which listeners read from the event.
Record<string, unknown>
A handler that never calls defaultHandler() replaces the behavior completely, so it must also return the shape callers expect.
Anchor to Auto-emitted eventsAuto-emitted events
updateCart emits the matching standard events as the call starts, before the cart is written. Each event carries a promise that settles with the outcome, so a listener can show a pending state. This happens whether or not the storefront configured the action, and without an eventTarget they dispatch from document:
| Action | Events auto-emitted |
|---|---|
openCart | None |
getCart | None |
updateCart |
|
An aborted call and an invalid payload don't emit shopify:cart:error.
eventTarget runs once per event, so each event dispatches from the element the function returns for that event. The action emits them itself, so your theme's own dispatch code only has to cover changes that happen outside an action.
Anchor to Preventing double UI updatesPreventing double UI updates
The action emits its cart events whether or not you write a handler. If your handler re-renders the cart and your components also re-render when they receive shopify:cart:lines-update, then both paths run and the UI updates twice.
A handler can add its own fields to the result, and listeners read them from the event's promise. A listener can then see what the handler already covered and skip its own render. This detail sits on the result, and is separate from the detail a caller attaches to the events:
A storefront whose components don't listen for cart events doesn't hit this, because the handler is the only thing updating the UI.