Skip to content

trackit/shopify-cart

Repository files navigation

@trackit.io/shopify-cart

A production-ready Shopify Storefront API cart library with optimistic updates, complete Cart API coverage, and zero-config React integration. Built for Next.js, React Router, TanStack Start, Expo, Vite, and plain React.

Installation

npm install @trackit.io/shopify-cart
yarn add @trackit.io/shopify-cart
pnpm add @trackit.io/shopify-cart

Examples

Try the live demos at shopify-cart.trackit.io, or run any example locally:

cd examples/tanstack-ssr  # or client-side, expo, next-ssr, react-router-ssr
pnpm install
pnpm dev
Example Live Local
Client-Side (Vite SPA) shopify-cart.trackit.io/client examples/client-side
Expo (React Native) shopify-cart.trackit.io/expo examples/expo
Next.js (App Router) shopify-cart.trackit.io/next examples/next-ssr
React Router v7 (SSR) shopify-cart.trackit.io/react-router examples/react-router-ssr
TanStack Start (SSR) shopify-cart.trackit.io/tanstack examples/tanstack-ssr

Try It Out — Discounts & Gift Cards

The demo store has pre-configured discounts and gift cards you can test in any example. Per-line discounts show up directly on each cart line — original price struck through, discounted price, and the coupon name with its impact.

Automatic Discounts (applied automatically when conditions are met):

Discount Condition
Wax discount Add any wax product to the cart
Buy 2 videographer board get 1 free Add 3 videographer boards

Discount Codes (enter in the discount code input):

Code Effect
TRACKIT25 25% off the entire order
COMPLETE10 10% off The Complete Snowboard

Gift Cards (enter in the same input — auto-detected):

Code Value
trackitgift15 $15 gift card
trackitgift30 $30 gift card

Gift cards stack — apply both for $45 off. They combine with discount codes too.

Table of Contents

Getting Started

Framework Guides

Reference

Features

  • Framework agnostic — Works with Next.js, React Router, TanStack Start, Expo, Vite, or plain React
  • Optimistic updates — Immediate UI feedback with automatic rollback on errors
  • Debounced operations — Batches rapid changes to reduce API calls
  • Action queue — Sequential cart operations prevent race conditions
  • Complete Storefront Cart API — Full implementation of every Shopify Storefront Cart query and mutation
  • Per-line discount allocations — Each cart line exposes its own discounts (title, type, savings amount)
  • Discount codes — Apply and remove discount codes with validation feedback
  • Gift cards — Add, remove, and stack multiple gift cards on a single cart
  • Shipping — Shipping line details with original/discounted prices and discount breakdowns
  • Delivery options — Select shipping methods from available delivery options per delivery group
  • Cart attributes — Add custom key-value attributes to the cart
  • Metafields — Set and delete cart metafields for extended cart data
  • Buyer identity — Update customer info, delivery address, and country for accurate pricing
  • Controlled cart identity — Persist cart IDs with your own storage strategy
  • TypeScript — Full type safety with exported types
  • Tree-shakeable — ESM with sideEffects: false

Architecture

The library supports two integration patterns depending on your framework.

Direct Pattern (Client-Side, Expo)

For SPAs and mobile apps, call the Shopify Storefront API directly — no backend needed:

graph LR
    subgraph Browser / App
        CP2[CartProvider] --> UC2[useCart] --> CAH2[createCartActionHandler]
    end
    CAH2 -->|GraphQL| SHOP2[Shopify Storefront API]
Loading

Use createCartActionHandler({ storeDomain, storefrontAccessToken }) and pass it to CartProvider.

SSR Pattern (Next.js, React Router, TanStack Start)

Server Actions are called directly from the client — no HTTP endpoints needed:

graph LR
    subgraph Browser
        CP1[CartProvider] --> UC1[useCart]
    end
    UC1 -->|Server Action| CA[cartAction]
    subgraph Server
        CA --> CAH1[createCartActionHandler]
    end
    CAH1 -->|GraphQL| SHOP1[Shopify Storefront API]
Loading

The onAction prop receives your Server Action directly. No createHttpCartAction needed.

Optimistic Updates

When a user interacts with the cart:

  1. Immediate feedback - UI updates instantly with pending state (line.isPending)
  2. Debouncing - Rapid changes are batched (default 300ms, configurable)
  3. Queue processing - Operations execute sequentially to prevent conflicts
  4. Reconciliation - Server response merges with any new pending operations
  5. Error recovery - Failed operations are rolled back automatically

Choose Your Framework

Direct Pattern (no backend needed) — calls Shopify Storefront API directly:

  • Client-Side — Vite, CRA, or any browser SPA
  • Expo — React Native with AsyncStorage

SSR Pattern (Server Actions) — no HTTP endpoints needed:

Common Patterns

The following helpers are used across multiple framework guides. Define them once in your project and import where needed.

Cookie Helpers (SSR frameworks)

SSR frameworks store the cart ID in cookies so it's accessible on both server and client:

function getCookie(name: string): string | null {
  const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
  return match?.[1] ?? null;
}

function setCookie(name: string, value: string | null) {
  if (value === null) {
    document.cookie = `${name}=; path=/; max-age=0`;
  } else {
    document.cookie = `${name}=${value}; path=/; max-age=${60 * 60 * 24 * 30}; samesite=lax`;
  }
}

Cart ID Persistence

CartProvider is storage-agnostic. Control cartId and onCartIdChange with whatever storage fits your app:

  • SSR web apps: cookies (see helpers above)
  • Browser SPA: localStorage
  • React Native/Expo: AsyncStorage

Client-Side

Direct Pattern — no backend, localStorage persistence, auto-loads cart on refresh

1. Create the Action Handler

// src/api.ts
import { createStorefrontClient } from "@trackit.io/shopify-cart";
import { createCartActionHandler } from "@trackit.io/shopify-cart/server";

const storeDomain = import.meta.env.VITE_SHOPIFY_STORE_DOMAIN;
const storefrontAccessToken = import.meta.env.VITE_SHOPIFY_STOREFRONT_ACCESS_TOKEN;

export const storefrontClient = createStorefrontClient({ storeDomain, storefrontAccessToken });
export const cartAction = createCartActionHandler({ storeDomain, storefrontAccessToken });

2. Set Up the Provider

import { useState, useCallback } from "react";
import { CartProvider } from "@trackit.io/shopify-cart/react";
import { cartAction } from "./api";

const CART_STORAGE_KEY = "shopify-cart-id";

function App() {
  const [cartId, setCartId] = useState<string | null>(() => localStorage.getItem(CART_STORAGE_KEY));

  const handleCartIdChange = useCallback((id: string | null) => {
    setCartId(id);
    if (id === null) {
      localStorage.removeItem(CART_STORAGE_KEY);
    } else {
      localStorage.setItem(CART_STORAGE_KEY, id);
    }
  }, []);

  return (
    <CartProvider cartId={cartId} onCartIdChange={handleCartIdChange} onAction={cartAction}>
      <Shop />
    </CartProvider>
  );
}

The cart auto-loads when cartId is set but no initialCart is provided. Use isLoading from useCart() to show a loading state.

3. Use the Cart

See Using the Cart for the useCart hook API.

Expo

Direct Pattern — no backend, AsyncStorage persistence, auto-loads cart on restart

1. Create the Action Handler

import { createStorefrontClient } from "@trackit.io/shopify-cart";
import { createCartActionHandler } from "@trackit.io/shopify-cart/server";

const storeDomain = process.env.EXPO_PUBLIC_SHOPIFY_STORE_DOMAIN;
const storefrontAccessToken = process.env.EXPO_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN;

const storefrontClient = createStorefrontClient({ storeDomain, storefrontAccessToken });
const cartAction = createCartActionHandler({ storeDomain, storefrontAccessToken });

2. Set Up the Provider

import { useState, useEffect, useCallback } from "react";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { CartProvider } from "@trackit.io/shopify-cart/react";

const CART_STORAGE_KEY = "shopify-cart-id";

export default function App() {
  const [cartId, setCartId] = useState(null);

  useEffect(() => {
    AsyncStorage.getItem(CART_STORAGE_KEY).then(setCartId);
  }, []);

  const handleCartIdChange = useCallback(async (id) => {
    setCartId(id);
    if (id) await AsyncStorage.setItem(CART_STORAGE_KEY, id);
    else await AsyncStorage.removeItem(CART_STORAGE_KEY);
  }, []);

  return (
    <CartProvider cartId={cartId} onCartIdChange={handleCartIdChange} onAction={cartAction}>
      <Shop />
    </CartProvider>
  );
}

The cart auto-loads when cartId is set but no initialCart is provided. Use isLoading from useCart() to show a loading state.

3. Use the Cart

See Using the Cart for the useCart hook API.

See the examples/expo directory for a complete working example.

Next.js

SSR Pattern — Server Actions, cookie persistence, SSR cart preloading

1. Create a Server Action

// app/actions.ts
"use server";

import {
  createCartActionHandler,
  type ProviderCartAction,
} from "@trackit.io/shopify-cart/server";

export const cartActionHandler = createCartActionHandler({
  storeDomain: process.env.SHOPIFY_STORE_DOMAIN!,
  storefrontAccessToken: process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN!,
});

export async function cartAction(action: ProviderCartAction) {
  return cartActionHandler(action);
}

2. Set Up the Provider

Use the cookie helpers from Common Patterns:

// app/providers.tsx
"use client";

import { useCallback, useState } from "react";
import { CartProvider, type CartState } from "@trackit.io/shopify-cart/react";
import { cartAction } from "./actions";

const CART_COOKIE_KEY = "shopify-cart-id";

export function Providers({
  children,
  initialCart,
}: {
  children: React.ReactNode;
  initialCart: CartState | null;
}) {
  const [cartId, setCartId] = useState<string | null>(
    () => initialCart?.id ?? getCookie(CART_COOKIE_KEY),
  );

  const handleCartIdChange = useCallback((id: string | null) => {
    setCartId(id);
    setCookie(CART_COOKIE_KEY, id);
  }, []);

  return (
    <CartProvider
      cartId={cartId}
      onCartIdChange={handleCartIdChange}
      onAction={cartAction}
      initialCart={initialCart}
    >
      {children}
    </CartProvider>
  );
}

3. Preload the Cart (SSR)

Load the cart on the server to avoid a flash of empty cart on hydration:

// app/layout.tsx
import { cookies } from "next/headers";
import { loadCart } from "@trackit.io/shopify-cart/server";
import { cartActionHandler } from "./actions";
import { Providers } from "./providers";

export default async function RootLayout({ children }) {
  const cookieStore = await cookies();
  const cartId = cookieStore.get("shopify-cart-id")?.value;
  const cart = await loadCart(cartActionHandler, cartId);

  return (
    <html>
      <body>
        <Providers initialCart={cart}>{children}</Providers>
      </body>
    </html>
  );
}

4. Use the Cart

See Using the Cart for the useCart hook API.

React Router

SSR Pattern — loaders, cookie persistence, SSR cart preloading

1. Create a Server Handler

// app/routes/api.cart.ts
import {
  createCartRequestHandler,
  createCartActionHandler,
} from "@trackit.io/shopify-cart/server";

export const cartActionHandler = createCartActionHandler({
  storeDomain: process.env.SHOPIFY_STORE_DOMAIN!,
  storefrontAccessToken: process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN!,
});

const cartRequestHandler = createCartRequestHandler(cartActionHandler);

export async function action({ request }: { request: Request }) {
  return cartRequestHandler(request);
}

2. Set Up the Root with SSR Preloading

Use the cookie helpers from Common Patterns:

// app/root.tsx
import { Outlet, useLoaderData, type LoaderFunctionArgs } from "react-router";
import { useCallback, useState } from "react";
import { loadCart } from "@trackit.io/shopify-cart/server";
import { createHttpCartAction } from "@trackit.io/shopify-cart/client";
import { CartProvider } from "@trackit.io/shopify-cart/react";
import { cartActionHandler } from "./routes/api.cart";

const CART_COOKIE_KEY = "shopify-cart-id";
const cartAction = createHttpCartAction("/api/cart");

export async function loader({ request }: LoaderFunctionArgs) {
  const cookieHeader = request.headers.get("Cookie") ?? "";
  const cartId = cookieHeader.match(/shopify-cart-id=([^;]+)/)?.[1];
  const cart = await loadCart(cartActionHandler, cartId);
  return { cart };
}

export default function App() {
  const { cart } = useLoaderData<typeof loader>();
  const [cartId, setCartId] = useState<string | null>(() => cart?.id ?? getCookie(CART_COOKIE_KEY));

  const handleCartIdChange = useCallback((id: string | null) => {
    setCartId(id);
    setCookie(CART_COOKIE_KEY, id);
  }, []);

  return (
    <CartProvider
      onAction={cartAction}
      cartId={cartId}
      onCartIdChange={handleCartIdChange}
      initialCart={cart}
    >
      <Outlet />
    </CartProvider>
  );
}

3. Use the Cart

See Using the Cart for the useCart hook API.

TanStack Start

SSR Pattern — server functions, cookie persistence, SSR cart preloading

1. Create Server Functions

TanStack Start requires using getCookie inside a server function:

// src/cart-actions.ts
import { createServerFn } from "@tanstack/react-start";
import { getCookie } from "@tanstack/react-start/server";
import {
  createCartActionHandler,
  loadCart,
} from "@trackit.io/shopify-cart/server";

export const cartActionHandler = createCartActionHandler({
  storeDomain: import.meta.env.VITE_SHOPIFY_STORE_DOMAIN!,
  storefrontAccessToken: import.meta.env.VITE_SHOPIFY_STOREFRONT_ACCESS_TOKEN!,
});

export const getInitialCart = createServerFn({ method: "GET" }).handler(
  async () => {
    const cartId = getCookie("shopify-cart-id");
    return loadCart(cartActionHandler, cartId);
  },
);

2. Set Up the Root with SSR Preloading

Use the cookie helpers from Common Patterns:

// src/routes/__root.tsx
import { createRootRoute } from "@tanstack/react-router";
import { useCallback, useState } from "react";
import { CartProvider } from "@trackit.io/shopify-cart/react";
import { getInitialCart, cartActionServer } from "../cart-actions";

const CART_COOKIE_KEY = "shopify-cart-id";

export const Route = createRootRoute({
  loader: async () => {
    const cart = await getInitialCart();
    return { cart };
  },
  component: RootComponent,
});

function RootComponent() {
  const { cart } = Route.useLoaderData();
  const [cartId, setCartId] = useState<string | null>(() => cart?.id ?? getCookie(CART_COOKIE_KEY));

  const handleCartIdChange = useCallback((id: string | null) => {
    setCartId(id);
    setCookie(CART_COOKIE_KEY, id);
  }, []);

  return (
    <CartProvider
      onAction={(action) => cartActionServer({ data: action })}
      cartId={cartId}
      onCartIdChange={handleCartIdChange}
      initialCart={cart}
    >
      <Outlet />
    </CartProvider>
  );
}

3. Use the Cart

See Using the Cart for the useCart hook API.

Using the Cart

Once your provider is set up, use the useCart hook in any component:

import { useCart } from "@trackit.io/shopify-cart/react";

function Shop() {
  const {
    cart,
    isLoading,
    isSyncing,
    addLine,
    updateLineQuantity,
    removeLine,
    applyCode,
    removeCode,
    clearAllCodes,
    updateBuyerIdentity,
  } = useCart();

  if (isLoading) return <span>Loading cart...</span>;

  const handleAddToCart = (variantId: string, product: ProductInfo) => {
    addLine(variantId, {
      title: product.title,
      variantTitle: product.variantTitle,
      price: product.price,
      image: product.image,
    });
  };

  return (
    <div>
      <p>Items: {cart.totalQuantity}</p>
      <p>
        Subtotal: {cart.subtotal.amount} {cart.subtotal.currencyCode}
      </p>
      <p>
        Total: {cart.total.amount} {cart.total.currencyCode}
      </p>
      {isSyncing && <span>Syncing...</span>}

      {/* Cart lines */}
      {cart.lines.map((line) => (
        <div key={line.id} style={{ opacity: line.isPending ? 0.7 : 1 }}>
          <span>
            {line.title} x {line.quantity}
          </span>
          <button
            onClick={() => updateLineQuantity(line.id, line.quantity + 1)}
          >
            +
          </button>
          <button
            onClick={() => updateLineQuantity(line.id, line.quantity - 1)}
          >
            -
          </button>
          <button onClick={() => removeLine(line.id)}>Remove</button>
        </div>
      ))}

      {/* Discount codes */}
      {cart.discountCodes.map((dc) => (
        <div key={dc.code}>
          {dc.code} {dc.applicable ? "(applied)" : "(not applicable)"}
          <button onClick={() => removeCode(dc.code, "discount")}>
            Remove
          </button>
        </div>
      ))}

      {/* Gift cards */}
      {cart.appliedGiftCards?.map((gc) => (
        <div key={gc.id}>
          Gift card ****{gc.lastCharacters} (-{gc.amountUsed.amount}{" "}
          {gc.amountUsed.currencyCode})
          <button onClick={() => removeCode(gc.id, "giftCard")}>Remove</button>
        </div>
      ))}

      {/* Shipping */}
      {cart.shipping && (
        <div>
          Shipping: {cart.shipping.title}{" "}
          {cart.shipping.discountedPrice.amount}{" "}
          {cart.shipping.discountedPrice.currencyCode}
        </div>
      )}

      {/* Delivery options */}
      {cart.deliveryGroups.map((group) => (
        <div key={group.id}>
          <h3>Delivery Group</h3>
          <p>
            Selected: {group.selectedDeliveryOption?.title ?? "None"}
          </p>
          {group.deliveryOptions.map((option) => (
            <button
              key={option.handle}
              onClick={() =>
                updateSelectedDeliveryOptions([
                  {
                    deliveryGroupId: group.id,
                    deliveryOptionHandle: option.handle,
                  },
                ])
              }
            >
              {option.title}{option.estimatedCost.amount}{" "}
              {option.estimatedCost.currencyCode}
            </button>
          ))}
        </div>
      ))}

      {/* Apply a code (auto-detects discount vs gift card) */}
      <button onClick={() => applyCode("SUMMER20")}>Apply Code</button>
      <button onClick={() => clearAllCodes()}>Clear All Codes</button>

      {/* Update buyer identity for accurate shipping/tax */}
      <button
        onClick={() =>
          updateBuyerIdentity({
            email: "customer@example.com",
            deliveryAddress: {
              address1: "123 Main St",
              city: "Ottawa",
              province: "ON",
              country: "CA",
              zip: "K1A 0A6",
            },
          })
        }
      >
        Set Address
      </button>

      {/* Cart attributes */}
      <button onClick={() => setAttribute("gift-wrap", "true")}>
        Add Gift Wrap
      </button>
      <button onClick={() => setAttribute("gift-message", "Happy Birthday!")}>
        Set Gift Message
      </button>
      <button onClick={() => removeAttribute("gift-wrap")}>
        Remove Gift Wrap
      </button>
      <p>Attributes: {cart.attributes.map((a) => `${a.key}=${a.value}`).join(", ")}</p>

      {/* Metafields */}
      <button
        onClick={() =>
          setMetafields([
            {
              ownerId: cart.id!,
              key: "custom.special_instructions",
              value: "Leave at door",
              type: "single_line_text_field",
            },
          ])
        }
      >
        Set Metafield
      </button>
    </div>
  );
}

Checkout

Once the cart is ready, use cart.checkoutUrl to send the user to Shopify's checkout. This library manages the cart — the checkout navigation is up to you.

Web (Next.js, React Router, TanStack Start, Vite)

Redirect or open the checkout URL in the current tab:

const { cart } = useCart();

<button
  disabled={!cart.checkoutUrl || cart.lines.length === 0}
  onClick={() => window.location.href = cart.checkoutUrl!}
>
  Checkout
</button>

React Native / Expo

For the best mobile experience, use Shopify's native checkout sheet via @shopify/checkout-sheet-kit. It opens Shopify checkout as an in-app modal with native UI:

pnpm add @shopify/checkout-sheet-kit
import { useShopifyCheckoutSheet } from "@shopify/checkout-sheet-kit";

function CheckoutButton() {
  const { cart } = useCart();
  const shopifyCheckout = useShopifyCheckoutSheet();

  return (
    <Button
      title="Checkout"
      disabled={!cart.checkoutUrl || cart.lines.length === 0}
      onPress={() => shopifyCheckout.present(cart.checkoutUrl!)}
    />
  );
}

You can also preload the checkout for faster open:

useEffect(() => {
  if (cart.checkoutUrl) {
    shopifyCheckout.preload(cart.checkoutUrl);
  }
}, [cart.checkoutUrl]);

If @shopify/checkout-sheet-kit is not available (e.g. Expo Go), fall back to opening the URL in the browser:

import * as Linking from "expo-linking";

Linking.openURL(cart.checkoutUrl!);

Custom Fetch

You can provide a custom fetch function to use an alternative HTTP client like axios. This is useful for adding interceptors, logging, or reusing an existing axios instance with configured timeouts and headers.

import axios from "axios";
import type { FetchFn } from "@trackit.io/shopify-cart/server";

const axiosFetch: FetchFn = async (input, init) => {
  const url = typeof input === "string" ? input : input.url;
  const response = await axios({
    url,
    method: (init?.method as string) ?? "POST",
    headers: init?.headers as Record<string, string>,
    data: init?.body,
  });

  return new Response(JSON.stringify(response.data), {
    status: response.status,
    headers: response.headers as HeadersInit,
  });
};

Use with createStorefrontClient:

const client = createStorefrontClient({
  storeDomain: "your-store.myshopify.com",
  storefrontAccessToken: "your-token",
  fetch: axiosFetch,
});

Or with createCartActionHandler:

const cartActionHandler = createCartActionHandler({
  storeDomain: "your-store.myshopify.com",
  storefrontAccessToken: "your-token",
  fetch: axiosFetch,
});

Or with createHttpCartAction:

const cartAction = createHttpCartAction({
  endpoint: "/api/cart",
  fetch: axiosFetch,
});

Development

Quick Start

# Install dependencies
pnpm install

# Start dev server (watch mode — rebuilds on changes)
pnpm run dev

# Build for production
pnpm run build

Contributing

We welcome issues and pull requests! Whether you're fixing bugs, adding features, or improving documentation:

  1. Open an issue to discuss ideas or report bugs before starting work
  2. Fork and create a branch following our conventions (feat/, fix/, docs/, etc.)
  3. Run tests locally before submitting:
    pnpm test       # Run tests in watch mode
    pnpm lint:fix   # Auto-fix linting issues
  4. Submit a PR with a clear description of changes

See the examples to test your changes across different frameworks (Next.js, React Router, Expo, etc.).

API Reference

Client

createHttpCartAction(endpoint, headers?)

Creates an action handler that sends cart operations to your API endpoint.

import { createHttpCartAction } from "@trackit.io/shopify-cart/client";

const cartAction = createHttpCartAction("/api/cart");
// or with headers
const cartAction = createHttpCartAction("/api/cart", {
  "X-Custom-Header": "value",
});
// or with options object (supports custom fetch)
const cartAction = createHttpCartAction({
  endpoint: "/api/cart",
  headers: { "X-Custom-Header": "value" },
  fetch: customFetch, // optional: custom fetch implementation
});

Server

createCartActionHandler(config)

Creates a handler function for cart operations. This is the core handler that can be used directly (Server Actions) or wrapped with createCartRequestHandler for HTTP endpoints.

import { createCartActionHandler } from "@trackit.io/shopify-cart/server";

const cartActionHandler = createCartActionHandler({
  storeDomain: "your-store.myshopify.com",
  storefrontAccessToken: "your-token",
});

// Use directly
const cart = await cartActionHandler({
  type: "load",
  cartId: "gid://shopify/Cart/123",
});

createCartRequestHandler(handler, options?)

Creates a Fetch API compatible HTTP handler that wraps a cart action handler.

import {
  createCartRequestHandler,
  createCartActionHandler,
} from "@trackit.io/shopify-cart/server";

const cartActionHandler = createCartActionHandler({
  storeDomain: "your-store.myshopify.com",
  storefrontAccessToken: "your-token",
});

const cartRequestHandler = createCartRequestHandler(cartActionHandler, {
  cors: { origin: ["https://example.com"] }, // optional
});

loadCart(handler, cartId)

Helper for SSR cart preloading. Loads a cart by ID with graceful error handling.

import { loadCart, createCartActionHandler } from "@trackit.io/shopify-cart/server";

const cartActionHandler = createCartActionHandler({ ... });

// Returns CartState or null (if cart not found or expired)
const cart = await loadCart(cartActionHandler, cartId);

createStorefrontClient(config)

Creates a typed GraphQL client for the Shopify Storefront API.

import { createStorefrontClient } from "@trackit.io/shopify-cart/server";

const client = createStorefrontClient({
  storeDomain: "your-store.myshopify.com",
  storefrontAccessToken: "your-token",
});

const result = await client.GetProducts({ first: 10 });

React

CartProvider

Context provider that manages cart state.

import { CartProvider } from "@trackit.io/shopify-cart/react";

<CartProvider
  onAction={cartAction} // Required: handles cart operations
  cartId={cartId} // Optional: controlled cart ID
  onCartIdChange={setCartId} // Optional: cart ID change callback
  initialCart={cart} // Optional: SSR initial state
  onError={handleError} // Optional: error callback
  debounceMs={300} // Optional: debounce delay (default: 300)
>
  {children}
</CartProvider>;

useCart()

Main hook for cart state and operations.

const {
  cart, // CartUIState - current cart state
  isLoading, // boolean - true while auto-loading an existing cart on mount
  isSyncing, // boolean - true when syncing with server
  pendingCount, // number - count of pending operations
  invalidCodeError, // string | null - error message when a code fails to apply
  addLine, // (merchandiseId: string, productInfo: ProductInfo) => void
  updateLineQuantity, // (lineId: string, quantity: number) => void
  removeLine, // (lineId: string) => void
  updateNote, // (note: string) => void
  applyCode, // (code: string) => void — tries discount first, then gift card
  removeCode, // (id: string, type: "discount" | "giftCard") => void
  clearAllCodes, // () => void — removes all discount codes and gift cards
  refreshCart, // () => Promise<void> — force refresh from server
  clearCart, // () => Promise<void> — remove all lines from cart
  updateBuyerIdentity, // (buyerIdentity: BuyerIdentityInput) => void
  getAttributes, // () => CartAttribute[] — get all cart attributes
  getAttribute, // (key: string) => string | undefined — get single attribute value
  setAttribute, // (key: string, value: string) => void — set single attribute
  removeAttribute, // (key: string) => void — remove single attribute
  updateAttributes, // (attributes: CartAttribute[]) => void — replace all attributes
  setMetafields, // (metafields: CartMetafieldInput[]) => void — set cart metafields
  deleteMetafield, // (ownerId: string, key: string) => void — delete cart metafield
  updateSelectedDeliveryOptions, // (options: SelectedDeliveryOptionInput[]) => void — select shipping methods
} = useCart();

Convenience Hooks

import {
  useCartLines, // () => CartLine[]
  useCartTotals, // () => { totalQuantity, subtotal }
  useAddToCart, // () => addLine function
  useUpdateLineQuantity, // () => updateLineQuantity function
  useRemoveFromCart, // () => removeLine function
  useUpdateNote, // () => updateNote function
  useRefreshCart, // () => refreshCart function
  useClearCart, // () => clearCart function
  // Note: Additional convenience hooks for attributes, metafields, and delivery options
  // can be created following the same pattern (useCart() + destructure specific methods)
} from "@trackit.io/shopify-cart/react";

Types

interface CartUIState {
  id: string | null;
  lines: CartLine[];
  totalQuantity: number;
  subtotal: Money;
  total: Money;
  shipping: ShippingLine | null;
  checkoutUrl: string | null;
  note: string | null;
  discountCodes: DiscountCode[];
  discountAllocations: DiscountAllocation[];
  groupedDiscountAllocations: GroupedDiscountAllocation[];
  appliedGiftCards?: AppliedGiftCard[];
  attributes: CartAttribute[];
  deliveryGroups: CartDeliveryGroup[];
}

interface CartLine {
  id: string;
  merchandiseId: string;
  quantity: number;
  title: string;
  variantTitle?: string;
  handle?: string;
  price: Money;                              // discounted unit price (after cart-level discounts)
  compareAtPrice?: Money;                    // variant's "compare at" price
  originalPrice?: Money;                     // variant's base price (before discounts)
  image?: { url: string; altText?: string };
  discountAllocations?: DiscountAllocation[]; // per-line discounts (title, amount, type)
  isPending?: boolean;                        // true for optimistic updates
}

interface Money {
  amount: string;
  currencyCode: string;
}

interface DiscountCode {
  code: string;
  applicable: boolean;
  isPending?: boolean;
}

interface DiscountAllocation {
  title: string;
  amount: Money;
  type?: "code" | "automatic";
  targetType?: "LINE_ITEM" | "SHIPPING_LINE";
}

interface GroupedDiscountAllocation {
  title: string;
  totalAmount: Money;
  type: "code" | "automatic";
  count: number;
  details: DiscountAllocation[];
  targetType?: "LINE_ITEM" | "SHIPPING_LINE";
}

interface AppliedGiftCard {
  id: string;
  lastCharacters: string;
  amountUsed: Money;
}

interface ShippingLine {
  title: string;
  originalPrice: Money;
  discountedPrice: Money;
  discounts?: ShippingDiscount[];
}

interface ShippingDiscount {
  title: string;
  amount: Money;
  type: "code" | "automatic";
}

interface BuyerIdentityInput {
  email?: string;
  phone?: string;
  countryCode?: string;
  customerAccessToken?: string;
  deliveryAddress?: DeliveryAddressInput;
}

interface DeliveryAddressInput {
  firstName?: string;
  lastName?: string;
  address1?: string;
  address2?: string;
  city?: string;
  province?: string;
  zip?: string;
  country?: string;
  phone?: string;
}

interface CartAttribute {
  key: string;
  value: string;
}

interface CartMetafieldInput {
  ownerId: string;
  key: string;
  value: string;
  type: string;
}

interface SelectedDeliveryOptionInput {
  deliveryGroupId: string;
  deliveryOptionHandle: string;
}

interface CartDeliveryOption {
  handle: string;
  title: string;
  estimatedCost: Money;
}

interface CartDeliveryGroup {
  id: string;
  selectedDeliveryOption: { handle: string; title: string } | null;
  deliveryOptions: CartDeliveryOption[];
}

interface ProductInfo {
  title: string;
  variantTitle?: string;
  handle?: string;
  price: Money;
  compareAtPrice?: Money;
  image?: { url: string; altText?: string };
}

interface CartError {
  code: CartErrorCode;
  message: string;
  cause?: unknown;
  context?: {
    cartId?: string | null;
    lineId?: string;
    merchandiseId?: string;
  };
}

type CartErrorCode =
  | "ADD_FAILED"
  | "UPDATE_FAILED"
  | "REMOVE_FAILED"
  | "NOTE_UPDATE_FAILED"
  | "DISCOUNT_UPDATE_FAILED"
  | "BUYER_IDENTITY_UPDATE_FAILED"
  | "LOAD_FAILED";

type ProviderCartAction =
  | { type: "add"; cartId: string | null; lines: CartLineInput[] }
  | { type: "update"; cartId: string; lines: CartLineUpdateInput[] }
  | { type: "remove"; cartId: string; lineIds: string[] }
  | { type: "load"; cartId: string }
  | { type: "updateNote"; cartId: string; note: string }
  | { type: "updateAttributes"; cartId: string; attributes: CartAttribute[] }
  | { type: "setMetafields"; cartId: string; metafields: CartMetafieldInput[] }
  | { type: "deleteMetafield"; cartId: string; ownerId: string; key: string }
  | { type: "updateSelectedDeliveryOptions"; cartId: string; selectedDeliveryOptions: SelectedDeliveryOptionInput[] }
  | { type: "updateDiscountCodes"; cartId: string; discountCodes: string[] }
  | { type: "addGiftCardCodes"; cartId: string; giftCardCodes: string[] }
  | { type: "updateGiftCardCodes"; cartId: string; giftCardCodes: string[] }
  | { type: "removeGiftCardCodes"; cartId: string; appliedGiftCardIds: string[] }
  | { type: "applyCode"; cartId: string; code: string }
  | { type: "removeCode"; cartId: string; code: string; codeType: "discount" | "giftCard" }
  | { type: "updateBuyerIdentity"; cartId: string; buyerIdentity: BuyerIdentityInput };

License

Apache-2.0

About

Shopify Cart NPM package

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors