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

CartItem

componentBeginnerserverbrowserlive demo

One line of the cart page: picture, name, prices, offers, a quantity input and a delete button that change the shopper's real cart.

import { CartItem } from '@salla.sa/twilight-theme-engine/components/cart';

In plain words

The cart page shows one CartItem per product in the cart: its picture, name, price, a quantity box, the line total and an × to remove it.

Changing the quantity or pressing × goes straight to the Salla SDK, which updates the real cart. The component does not fetch the cart: you pass it one item from a cart you already read.

It is lazy, so it goes inside <Suspense>.

Signature

const CartItem: React.LazyExoticComponent<(props: {
  item: CartItem;      // the CartItem type from @salla.sa/twilight-theme-engine/types
  isFirst?: boolean;   // false: eager image for the first line
}) => JSX.Element>

Try it live

Your own guest cart on the demo store, one CartItem per line. Changing a quantity or deleting a line changes that real cart.Try this: raise a quantity: the line total changes only because this demo re-reads the cart on the SDK updated event.
Real requests to the demo store
Storefront canvas · en · LTR
Runs in the browser…
What a theme writes
import { Suspense } from 'react';
import { CartItem } from '@salla.sa/twilight-theme-engine/components/cart';
import type { Cart } from '@salla.sa/twilight-theme-engine/types';

export function CartLines({ cart, readAt }: { cart: Cart; readAt: number }) {
  return (
    <Suspense fallback={null}>
      {/* CartItem is lazy. A new key per read: a deleted line has already removed its own form. */}
      <div key={readAt}>
        {cart.items.map((item, index) => (
          <CartItem key={item.id} item={item} isFirst={index === 0} />
        ))}
      </div>
    </Suspense>
  );
}

Example

app/components/cart/CartLines.tsx
import { Suspense } from 'react';
import { CartItem } from '@salla.sa/twilight-theme-engine/components/cart';
import type { Cart } from '@salla.sa/twilight-theme-engine/types';

export function CartLines({ cart }: { cart: Cart }) {
  return (
    <Suspense fallback={null}>
      {cart.items.map((item, index) => (
        <CartItem key={item.id} item={item} isFirst={index === 0} />
      ))}
    </Suspense>
  );
}

How it behaves

  • Each line is its own <form id="item-<id>">. A quantity change calls Salla.form.onChange('cart.updateItem', event), and the SDK reads the hidden id and the quantity input. Do not put it inside another <form>.

  • × calls Salla.cart.deleteItem(item.id).

  • Prices may be numbers or { amount, currency } objects; each is formatted with useMoney().format in its own currency. The total is total_special_price when the line has detailed_offers, else total, or "Out of Stock" when is_available is false.

  • Reading the cart: the engine's cart page gets the id with Salla.cart.api.getCurrentCartId() in an effect (it creates a guest cart when there is none), then useQuery(cart.queries.detail(id)) from /api/cart. The id lives in the browser, so the server render has no cart.

Gotchas

  • Nothing re-reads the cart after the SDK changes it: nothing in the engine subscribes to cart events or refetches ['cart', id], so line totals and CartSummary keep the numbers the page loaded with. Refetch on Salla.cart.event.onUpdated(callback) (and remove it with offUpdated(callback)), as the demo does.

  • × removes the line with document.querySelector('#item-<id>')?.remove() behind React's back. When you later re-render the list without that item, React tries to remove a form that is already gone. Render the list under a key that changes with each fresh read (the demo uses dataUpdatedAt) so React replaces the whole list instead.

  • The component shares its name with the CartItem type from /types. Rename one on import: import type { CartItem as CartLine } from '@salla.sa/twilight-theme-engine/types'.

Related

Source and docs