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

product.list

objectBeginnerserverbrowserlive demo

Lists products from any source (latest, offers, a category, a brand, a search) with page size, sort, filters and cursor pagination.

import { product, ProductsListParams, ProductsListResult, ProductsListSource, ProductsListResponse, Filter, FilterValue } from '@salla.sa/twilight-theme-engine/api/product';

In plain words

A storefront shows lists of products everywhere: the newest arrivals, today's offers, everything in one category. product.list() asks the Salla API for one such list and hands back { items, next }: the products, and a marker for the next page (null on the last one).

Every engine API module comes in two forms. The plain async functions (product.list(...)) return a Promise you can await anywhere, for example in a loader (the function a route runs to fetch its data before the page renders). The query options (product.queries.list(...)) are the same request packaged for TanStack Query, the caching library the engine uses: pass them to its useQuery hook inside a component and you get loading states and caching for free.

Signature

product.list(params: ProductsListParams): Promise<ProductsListResult>
product.queries.list(params: Omit<ProductsListParams, 'signal'>)
  // queryOptions, key ['products', 'list', params, { scope }]

interface ProductsListParams {
  source: ProductsListSource;
  sourceValue?: string | number | number[] | null;  // the ids, as an array
  keyword?: string;          // the search words, when source is 'search'
  perPage?: number;          // default 16
  page?: number;             // sent only when given
  cursor?: string | null;
  sort?: string;             // 'ourSuggest' | 'bestSell' | 'topRated' | 'priceFromLowToTop' | 'priceFromTopToLow'
  filterable?: boolean;
  signal?: AbortSignal;
}

type ProductsListSource =
  | 'latest' | 'offers' | 'sales' | 'best_selling' | 'top-rated'
  | 'categories' | 'brands' | 'tags' | 'search' | 'selected' | 'related'
  | 'wishlist' | 'recently' | 'reorder' | 'json' | 'landing-page';

type ProductsListResult = PaginatedResult<Product> & { filters?: Filter[] };
// { items: Product[]; next: string | null; filters?: Filter[] }

interface ProductsListResponse { data: Product[]; cursor?: Pagination; filters?: Filter[] }  // the raw body
interface Filter { key?: string; label?: string; type?: string; values?: FilterValue[]; min?: number; max?: number }
interface FilterValue { key?: string; value?: string; count?: number; from?: number | '*'; to?: number | '*' }

Try it live

product.queries.list() against the demo store: pick where products come from, how many, and in which order.Try this: press Next page and read next: it is a whole URL. Then pick categories with no ids to see the API refuse it.
Storefront canvas · ar · RTL

Loading products…

Controls
latest reads no ids.
The API default is 16.
filterableAdds the filter facets to the result.
What a theme writes
import { useQuery } from '@tanstack/react-query';
import { product } from '@salla.sa/twilight-theme-engine/api/product';

export function ProductRow() {
  const { data, isPending } = useQuery(product.queries.list({ source: 'latest', perPage: 4 }));
  if (isPending) return <p>Loading…</p>;

  return (
    <ul className="product-row">
      {data?.items.map((item) => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

Example

app/components/home/OffersRow.tsx
import { useQuery } from '@tanstack/react-query';
import { product } from '@salla.sa/twilight-theme-engine/api/product';
import { ProductCard } from '@salla.sa/twilight-theme-engine/components/product';

export function OffersRow() {
  const { data, isPending } = useQuery(product.queries.list({ source: 'offers', perPage: 8 }));
  if (isPending) return <p>Loading offers…</p>;

  return (
    <div className="s-products-list-wrapper s-products-list-vertical-cards">
      {data?.items.map((item, index) => (
        <ProductCard key={item.id} product={item} index={index} />
      ))}
    </div>
  );
}

How it behaves

  • Endpoint: GET products?source=…&per_page=…. keyword is sent as keyword when source is search; sourceValue is sent as source_value[], once per id. filterable sends 1 or 0; page, sort and cursor are sent only when given.

  • What each source reads: categories numeric category ids (sourceValue: [Category.id_]), brands brand ids, tags tag ids, selected product ids, related one product id (sourceValue: [product.id], the API reads the first), and search its words (keyword: 'كرسي'). latest, offers, sales and top-rated read nothing. Without what it reads, the API answers HTTP 422 and the call throws (demo store: categories and search).

  • Every product in the result is also remembered as a list-card preview, so product.queries.detail(id) for one of them paints at once. See the preview cache.

  • The query key holds the whole params object plus the active branch scope, so { source: 'latest' } and { source: 'latest', perPage: 16 } are two cache entries although they ask the same thing. The queryFn passes TanStack Query's AbortSignal on, so an unneeded request is cancelled.

  • filters comes back only with filterable: true (the demo store answers null otherwise). There is no parameter to apply a chosen filter.

  • Public endpoint for catalogue sources; runs on the server and in the browser. Inside the engine router the QueryClient defaults are staleTime 60 s, gcTime 5 min, retry 1 and no refetch on window focus.

Gotchas

  • next is a whole URL (https://api.salla.dev/store/v1/products?…&cursor=…&page=2), not the opaque token api/types describes. Passed back unchanged as cursor, the demo store answered HTTP 200 with no data, so product.list resolves { items: [], next: null } and a Load more button silently stops. Send only the parameter: new URL(next).searchParams.get('cursor'), as the engine's ProductListPage does.

  • The JSDoc on ProductsListParams.perPage says the default is 15; product.list sends 16. (wishlist.list is the one that defaults to 15.)

  • best_selling is in ProductsListSource, but the API does not know it and answers with latest (the same products in the same order on the demo store). For best sellers use sales, which the API sorts by sales.

  • Product.id is typed number, but the products endpoint sends ids as strings ("677709975"). Pass String(item.id) to product.find and product.queries.detail, and compare ids as strings.

  • For categories the hashed Category.id ("mgjZqj") is refused with HTTP 422; use the numeric id_.

Related

Source and docs