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

product.find

objectBeginnerserverbrowserlive demo

Fetches one product with everything a product page needs: images, options, SKUs, brand, tags, rating and bundle contents.

import { product } from '@salla.sa/twilight-theme-engine/api/product';

In plain words

A product page needs far more than a product card: every photo, the size and color options, stock per option. product.find(id) loads all of that for one product id.

product.findOrThrow(id) does the same for a loader but turns any failure into the store's 404 page, and product.queries.detail(id) is the version for useQuery in a component. While the details load, that query shows the product card you already have from a list, so the page never starts blank.

Signature

product.find(id: string): Promise<Product>
product.findOrThrow(id: string): Promise<Product>   // any failure → NotFoundError (the 404 page)
product.queries.detail(id: string)
  // queryOptions, key ['products', 'detail', id, { scope }]
  //               placeholderData: () => getProductPreview(id)

Try it live

product.queries.detail(id) for one of the latest products. The list card paints first, then the full details replace it.Try this: pick a product you have not opened yet and watch isPlaceholderData flip to false as images and options arrive.
Storefront canvas · en · LTR

Loading the latest products…

Controls
What a theme writes
import { useQuery } from '@tanstack/react-query';
import { product } from '@salla.sa/twilight-theme-engine/api/product';

export function ProductSummary({ id }: { id: string }) {
  const { data, isPlaceholderData } = useQuery(product.queries.detail(id));
  if (!data) return <p>Loading…</p>;

  return (
    <article aria-busy={isPlaceholderData}>
      <h2>{data.name}</h2>
      {/* A list card stands in until the details arrive: it has no images[] yet. */}
      {isPlaceholderData ? <p>Loading details…</p> : <p>{data.images?.length ?? 0} photos</p>}
    </article>
  );
}

Example

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

export function QuickView({ productId }: { productId: string }) {
  const { format } = useMoney();
  const { data, isPlaceholderData, isError } = useQuery(product.queries.detail(productId));

  if (isError) return <p>This product is no longer available.</p>;
  if (!data) return <p>Loading…</p>;

  return (
    <div className="quick-view">
      <img src={data.image.url} alt={data.image.alt ?? data.name} />
      <h2>{data.name}</h2>
      <p>{format(data.price)}</p>
      {!isPlaceholderData && <p>{data.options?.length ?? 0} options to choose from</p>}
    </div>
  );
}

How it behaves

  • Endpoint: GET products/{id}/details with with[]= images, category, rating, notify_availability, tags, brand, options, skus, skus_availability, included_products, bundle and sold_quantity. Public.

  • The engine product page loader calls product.findOrThrow(params.id). findOrThrow is orThrow(product.find(id)), so a timeout or a 500 also becomes the 404 page. Call find and catch yourself when those must look different.

  • The detail key includes the active branch scope, because stock and availability are answered per branch.

  • placeholderData is a stand-in, not cached data: isPlaceholderData is true and the request still runs. The stand-in is a list card, so fields only this endpoint sends (images, options, skus) are missing until it resolves.

Gotchas

  • Options from this endpoint carry their choices in options[].details; options[].values (marked as the listing shape in the type) is absent. Code written for values finds nothing on a product page.

  • Reading data.images while isPlaceholderData is true gives undefined, not an empty gallery that later grows. Render the card fields first and wait for isPlaceholderData === false before anything that needs the details.

Related

Source and docs