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

Product preview cache

functionAdvancedserverbrowserlive demo

The list-card cache that lets a product's details paint at once from the card you already have, before the full details arrive.

import { rememberProductPreview, getProductPreview, productPreviewQueryKey, productDetailQueryKey } from '@salla.sa/twilight-theme-engine/api/product';

In plain words

A product list already knows each product's name, price and image. When a shopper opens one, the engine shows that card data right away and fills in the rest (photos, options) when the details arrive.

These four functions run that cache. product.list and wishlist.list feed it for you. Call rememberProductPreview(product) when your theme gets products some other way.

Signature

function rememberProductPreview(product: Product): void
function getProductPreview(id: string): Product | undefined
function productPreviewQueryKey(id: string):
  readonly ['products', 'preview', string, { scope: ActiveScope | null }]
function productDetailQueryKey(id: string):
  readonly ['products', 'detail', string, { scope: ActiveScope | null }]

Try it live

The engine's list-card cache, live: what getProductPreview() finds for one of the latest products, and where.Try this: press Forget the preview: it is still found, rebuilt from the cached list. Fetch the details, then forget again.
Storefront canvas · en · LTR
Runs in the browser…
Controls
What a theme writes
import { useQuery } from '@tanstack/react-query';
import { api } from '@salla.sa/twilight-theme-engine/api/client';
import { rememberProductPreview } from '@salla.sa/twilight-theme-engine/api/product';
import type { Product } from '@salla.sa/twilight-theme-engine/types';

/** Products fetched without product.list: remember them so a quick view paints at once. */
export function useOfferCards() {
  return useQuery({
    queryKey: ['offer-cards'],
    queryFn: async () => {
      const { data } = await api.get('products?source=offers&per_page=8').json<{ data: Product[] }>();
      data.forEach(rememberProductPreview);
      return data;
    },
  });
}

Example

app/hooks/useOfferCards.ts
import { useQuery } from '@tanstack/react-query';
import { api } from '@salla.sa/twilight-theme-engine/api/client';
import { rememberProductPreview } from '@salla.sa/twilight-theme-engine/api/product';
import type { Product } from '@salla.sa/twilight-theme-engine/types';

/** Products fetched without product.list: remember them so a quick view paints at once. */
export function useOfferCards() {
  return useQuery({
    queryKey: ['offer-cards'],
    queryFn: async () => {
      const { data } = await api
        .get('products?source=offers&per_page=8')
        .json<{ data: Product[] }>();
      data.forEach((item) => rememberProductPreview(item));
      return data;
    },
  });
}

How it behaves

  • rememberProductPreview writes the card under the preview key of the engine QueryClient (from the twilight context). It does nothing when the details for that id are already cached successfully, or when there is no QueryClient.

  • Previews live under their own key so a card never counts as a successful details fetch: product.queries.detail(id) still requests the details and uses the card only as placeholderData.

  • getProductPreview reads the preview key first. When it is empty (previews are left out of the server-rendered HTML), it scans the cached ['products', 'list'] and ['wishlist', 'list'] queries of the same branch scope, remembers what it finds, and returns the match.

  • productPreviewQueryKey wraps the id in String(); productDetailQueryKey uses it as given. Pass strings to both.

Gotchas

  • A list query whose key does not end with a { scope } object never feeds previews: getProductPreview skips it, as it skips lists cached under another branch. A custom list query should follow the engine's key shape, or call rememberProductPreview itself.

  • Both keys read the active scope when called; a key built before a branch change no longer matches the cache.

Related

Source and docs