product.list
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
Loading products…
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
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=….keywordis sent askeywordwhensourceissearch;sourceValueis sent assource_value[], once per id.filterablesends1or0;page,sortandcursorare sent only when given.What each source reads:
categoriesnumeric category ids (sourceValue: [Category.id_]),brandsbrand ids,tagstag ids,selectedproduct ids,relatedone product id (sourceValue: [product.id], the API reads the first), andsearchits words (keyword: 'كرسي').latest,offers,salesandtop-ratedread nothing. Without what it reads, the API answers HTTP 422 and the call throws (demo store:categoriesandsearch).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'sAbortSignalon, so an unneeded request is cancelled.filterscomes back only withfilterable: true(the demo store answersnullotherwise). 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
staleTime60 s,gcTime5 min,retry1 and no refetch on window focus.
Gotchas
nextis a whole URL (https://api.salla.dev/store/v1/products?…&cursor=…&page=2), not the opaque tokenapi/typesdescribes. Passed back unchanged ascursor, the demo store answered HTTP 200 with nodata, soproduct.listresolves{ items: [], next: null }and a Load more button silently stops. Send only the parameter:new URL(next).searchParams.get('cursor'), as the engine'sProductListPagedoes.The JSDoc on
ProductsListParams.perPagesays the default is 15;product.listsends 16. (wishlist.listis the one that defaults to 15.)best_sellingis inProductsListSource, but the API does not know it and answers withlatest(the same products in the same order on the demo store). For best sellers usesales, which the API sorts by sales.Product.idis typednumber, but the products endpoint sends ids as strings ("677709975"). PassString(item.id)toproduct.findandproduct.queries.detail, and compare ids as strings.For
categoriesthe hashedCategory.id("mgjZqj") is refused with HTTP 422; use the numericid_.
Related
Fetches one product with everything a product page needs: images, options, SKUs, brand, tags, rating and bundle contents.
PaginatedResult, PaginationThe shared pagination shapes: the API's cursor block, and the { items, next } result that list functions return.
Product preview cacheThe list-card cache that lets a product's details paint at once from the card you already have, before the full details arrive.
ProductCardThe product tile of every grid: image, badge, name, price, rating, wishlist heart and a real Add to cart button, in five layouts.