---
title: Configure actions
description: >-
  Replace the default behavior of a standard storefront action so it updates
  your own UI instead of reloading the page.
source_url:
  html: 'https://shopify.dev/docs/api/storefront-events-and-actions/actions/configure'
  md: >-
    https://shopify.dev/docs/api/storefront-events-and-actions/actions/configure.md
api_name: storefront-events-and-actions
---

# 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](#how-the-defaults-recognize-a-theme). 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.

***

## What the defaults do

Without a configuration, each action runs its default behavior:

* **[`updateCart`](https://shopify.dev/docs/api/storefront-events-and-actions/actions/update-cart)**: Writes to the Storefront API, then refreshes the cart in place on a [theme it recognizes](#how-the-defaults-recognize-a-theme). If it recognizes none, then it falls back to a full page reload.
* **[`openCart`](https://shopify.dev/docs/api/storefront-events-and-actions/actions/open-cart)**: Opens a cart drawer it finds on the page, and otherwise navigates to `/cart`.
* **[`getCart`](https://shopify.dev/docs/api/storefront-events-and-actions/actions/get-cart)**: Reads the current cart from the Storefront API without affecting the page.

**Note:**

`getCart` isn't configurable. Calling `Shopify.actions.getCart.configure(...)` is a TypeScript error and a runtime `TypeError`.

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

***

## Configuring 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:

```javascript
document.addEventListener('DOMContentLoaded', () => {
  Shopify.actions.updateCart.configure({
    eventTarget: () => document.querySelector('cart-items'),
  });
});
```

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.

#### Configuration options

* **event​Target**

  **(meta: UpdateCartEventTargetMeta) => EventTarget | null**

  A function that returns the element that auto-emitted events dispatch from. Required when you configure `updateCart`, which is the only action that emits events. `openCart` doesn't accept it. Returning `null` dispatches from `document`.

* **handler**

  **(defaultHandler, ...args) => Promise\<Result>**

  Replaces the action's behavior. Call `defaultHandler()` 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.

  ```ts
  '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\`.

  ```ts
  'add' | 'remove' | 'update'
  ```

***

## event​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:

```javascript
Shopify.actions.updateCart.configure({
  eventTarget: (meta) => {
    if (meta.type === 'shopify:cart:note-update') return document.querySelector('cart-note');
    if (meta.type === 'shopify:cart:discount-update') return document.querySelector('cart-discount');
    if (meta.type === 'shopify:cart:lines-update' && meta.action === 'add') {
      return document.querySelector('product-form');
    }
    return document.querySelector('cart-items');
  },
});
```

That routing is possible because the function receives a `meta` object describing the event it's about to emit:

#### meta

* **type**

  **'shopify:cart:lines-update' | 'shopify:cart:note-update' | 'shopify:cart:discount-update' | 'shopify:cart:error'**

  **required**

  The event the action is about to emit.

* **action**

  **'add' | 'remove' | 'update'**

  Which kind of line change is happening. Only present when `type` is `shopify:cart:lines-update`.

If the function returns `null`, then the event dispatches from `document` instead.

***

## handler

`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`](https://shopify.dev/docs/api/storefront-events-and-actions/actions/open-cart) 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:

```javascript
document.addEventListener('DOMContentLoaded', () => {
  Shopify.actions.updateCart.configure({
    eventTarget: (meta) => {
      if (meta.type === 'shopify:cart:lines-update' && meta.action === 'add') {
        return document.querySelector('product-form');
      }
      return document.querySelector('cart-items');
    },
    async handler(defaultHandler, payload) {
      const result = await defaultHandler();
      const response = await fetch(window.location.pathname + '?sections=cart-drawer');
      const { 'cart-drawer': html } = await response.json();
      document.querySelector('cart-drawer').innerHTML = html;
      return result;
    },
  });


  Shopify.actions.openCart.configure({
    handler() {
      document.querySelector('cart-drawer')?.open();
    },
  });
});
```

A handler receives `defaultHandler` first, followed by the same arguments the caller passed to the action:

#### Handler arguments

* **default​Handler**

  **() => Promise\<Result>**

  **required**

  Runs Shopify's implementation and resolves with the result the action would have returned on its own.

* **payload**

  **UpdateCartPayload**

  The payload the caller passed. `openCart` takes no payload.

* **options**

  **UpdateCartOptions**

  The options the caller passed, such as an `AbortSignal`. `openCart` 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.

  ```ts
  string
  ```

* lines

  The lines to add, update, or remove.

  ```ts
  CartLineInput[]
  ```

* note

  A new cart note.

  ```ts
  string
  ```

* discountCodes

  The complete set of discount codes the cart should end up with.

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

  ```ts
  string
  ```

* merchandiseId

  The product variant GID, or a raw variant ID. Required when adding a line.

  ```ts
  string
  ```

* quantity

  The quantity for the line. Set it to \`0\` to remove the line.

  ```ts
  number
  ```

* attributes

  Custom key-value pairs to attach to the line.

  ```ts
  Array<{ key: string, value: string }>
  ```

* sellingPlanId

  The selling plan GID, or a raw selling plan ID, for a subscription or other selling plan.

  ```ts
  string
  ```

### UpdateCartOptions

Options for an \`updateCart\` call.

* signal

  Aborts the request when the signal fires.

  ```ts
  AbortSignal
  ```

* event

  Shapes the events the call emits.

  ```ts
  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\`.

  ```ts
  'product' | 'cart' | 'dialog' | 'standard-action'
  ```

* detail

  Custom data attached to the emitted events, which listeners read from the event.

  ```ts
  Record<string, unknown>
  ```

A handler that never calls `defaultHandler()` replaces the behavior completely, so it must also return the shape callers expect.

***

## Auto-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` | * [`shopify:cart:lines-update`](https://shopify.dev/docs/api/storefront-events-and-actions/events/cart-lines-update), if the payload includes lines
* [`shopify:cart:note-update`](https://shopify.dev/docs/api/storefront-events-and-actions/events/cart-note-update), if the payload includes a note
* [`shopify:cart:discount-update`](https://shopify.dev/docs/api/storefront-events-and-actions/events/cart-discount-update), if the payload includes discount codes
* [`shopify:cart:error`](https://shopify.dev/docs/api/storefront-events-and-actions/events/cart-error), if the request failed |

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.

***

## Preventing 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](https://shopify.dev/docs/api/storefront-events-and-actions/events/listen#waiting-for-the-result). 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](https://shopify.dev/docs/api/storefront-events-and-actions/actions/call#adding-context-to-emitted-events):

```javascript
// In the handler: mark what it already rendered.
async handler(defaultHandler, payload) {
  const result = await defaultHandler();
  await renderCartSections();
  return { ...result, detail: { handledBy: 'theme' } };
}


// In the component: skip the render the handler covered.
event.promise.then(({ detail }) => {
  if (detail?.handledBy === 'theme') return;
  morphSection(sectionId);
});
```

A storefront whose components don't listen for cart events doesn't hit this, because the handler is the only thing updating the UI.

***

## Next steps

[Actions reference\
\
](https://shopify.dev/docs/api/storefront-events-and-actions/actions)

[Look up the parameters and resolved value for every action.](https://shopify.dev/docs/api/storefront-events-and-actions/actions)

[Dispatch events\
\
](https://shopify.dev/docs/api/storefront-events-and-actions/events/dispatch)

[Emit events from your storefront when commerce interactions happen.](https://shopify.dev/docs/api/storefront-events-and-actions/events/dispatch)

***
