Skip to content
Twilight React Playground
ثيم رائدaren

Cart

route-moduleBeginnerserverbrowserlive demo

The cart page, whose loader returns only a title because the visitor's cart id exists in the browser, not on the server.

import { Cart, CartPage, cartLoader, CartPageProps } from '@salla.sa/twilight-theme-engine/routes/cart';

In plain words

Every visitor has their own cart, and the Salla SDK keeps its id in the browser. The server that builds the page cannot know it, so Cart.loader returns just the page title and the server's HTML shows a skeleton.

Once the page runs in the browser, CartPage asks the SDK for the cart id, loads the cart and draws the items, the coupon box and the order summary. Open it with your own cart on Built-in pages.

Signature

const Cart: {
  readonly id: 'cart';
  readonly loader: (
    ctx?: { locale?: string },
    extend?: (data: CartPageProps, ctx: { params: {} }) => Record<string, unknown> | Promise<Record<string, unknown>>
  ) => Promise<CartPageProps>;   // a promise returned by extend is ignored
  readonly head: (ctx: TwilightContext, data: CartPageProps) => HeadDescriptor;
  readonly Component: typeof CartPage;
};

function cartLoader(): CartPageProps;                // synchronous
function CartPage(props: CartPageProps): JSX.Element;

interface CartPageProps {
  page: Page;    // { slug: 'cart', title }
  cart?: Cart;   // never set by the loader, not read by CartPage
}

Try it live

The engine's cart page with your own visitor's cart, and the skeleton the server sends in its place.Try this: add a product to the cart from any product page, come back, and the items appear here. Change the title: it is the last breadcrumb.
Real requests to the demo store
Storefront canvas · ar · RTL
Controls
What a theme writes
import { CartPage } from '@salla.sa/twilight-theme-engine/routes/cart';

// A custom checkout step that reuses the engine's cart page as it is.
export function CheckoutCart() {
  return <CartPage page={{ slug: 'cart', title: 'Cart' }} />;
}

Example

app/components/CheckoutCart.tsx
import { CartPage } from '@salla.sa/twilight-theme-engine/routes/cart';

// A custom page that reuses the engine's cart as it is.
export function CheckoutCart() {
  return <CartPage page={{ slug: 'cart', title: 'Your bag' }} />;
}

How it behaves

  • URL: /{-$locale}/cart. The generated route shows CartSkeleton while loading (see Route modules for the file).

  • cartLoader() returns { page: { slug: 'cart', title } } with the common.titles.cart translation (or Cart). It reads the i18n instance from the router's request context, so call it from a loader.

  • head sets the title and hreflang alternates, plus a description only when page.description is set, which the loader never does. There is no canonical.

  • CartPage is not lazy. In the browser it reads loyalty.queries.points() at once and, after mounting, calls window.Salla.cart.api.getCurrentCartId() and then loads cart.queries.detail(id) with TanStack Query. It renders CartSkeleton until the id and the cart arrive; an empty cart shows NoContent with a link home.

  • Hook slots: cart:start, cart:items.start (context { cartItems }), cart:items.end and cart:end. It also renders Salla's conditional-offer and offer web components, SeoCartWidget for the analytics events, and CartSummary with the store's coupon setting.

Gotchas

  • Without the Salla SDK (window.Salla missing, for example when its script is blocked) the cart id is never requested and the skeleton stays forever.

  • Cart.loader ignores an extend that returns a promise: it merges only a plain object (extra instanceof Promise ? {} : extra), with no warning. Return an object synchronously, or call Cart.loader inside your own async loader and spread the result.

  • docs/08-page-customization.md shows a cart route whose loader fetches the cart (loader: async () => ({ cart: await getCart() })). On the server there is no cart id to fetch; keep loading the cart in the browser, as CartPage does.

Related

Source and docs