Loading data
Fetch store data in a route loader, and reuse it in components with TanStack Query.
Read this when a page needs store data such as products, categories or brands. The engine gives you two tools: loaders that run before the page renders, and API modules that know how to talk to the store.
What SSR means here
SSR (server-side rendering) means the server builds the page's HTML before the browser gets it. Data fetched in a route loader is part of that HTML, so customers and search engines see content on the first paint, with no spinner. Data fetched later in the browser is not.
Default to a loader for anything the page needs to show. Use the browser only for things that change from user action, like the cart. See Salla SDK and events.
A loader with an API module
A loader returns the data the page component renders. It runs on the server for the first load,
and in the browser when the customer navigates inside the site. The engine's API modules are separate imports under
@salla.sa/twilight-theme-engine/api/*, so you only bundle what you use.
import { createFileRoute } from '@tanstack/react-router';
import { brand } from '@salla.sa/twilight-theme-engine/api/brands';
export const Route = createFileRoute('/{-$locale}/our-brands')({
loader: () => brand.list(),
component: OurBrands,
});
function OurBrands() {
// brand.list() returns { items, next }: one page of brands and a cursor for the next page
const { items } = Route.useLoaderData();
return (
<ul>
{items.map((b) => (
<li key={b.id}>
<img src={b.logo} alt={b.name} width="200" height="150" />
</li>
))}
</ul>
);
}Add route('/our-brands', 'our-brands.tsx') to app/routes.ts, as shown in Routes and pages.
Here is a real loader from the starter theme. It uses the page API and throws a 404 if the page is missing:
import { createFileRoute } from '@tanstack/react-router';
import { page as pageApi } from '@salla.sa/twilight-theme-engine/api/page';
export const Route = createFileRoute('/{-$locale}/$slug/page-{$id}')({
loader: async ({ params }) => {
const data = await pageApi.findOrThrow(params.id);
return { page: { id: data.id, title: data.name, content: data.content } };
},
// …
});Reading API modules
Each module exports one object, such as product, category, brand, page, cart, menu or store.
Common names repeat across modules:
find(id)reads one record.list(...)reads many.findOrThrow(id)does the same but throws, so a missing record becomes a 404 page. Prefer it in loaders. It exists onproduct,category,brand,page,blogandorder.queriesholds ready-made TanStack Query options for the same calls.
Method names are not identical everywhere (cart.get takes a cart id, product.list takes a source), so check the
API tab before you call one.
Using the same data in a component
TanStack Query is the library that caches fetched data. The engine creates one query client for loaders and components,
so a component using queries shares the cache instead of fetching again. This real component from the starter theme
loads a product slider after the page has rendered:
import { useQuery } from '@tanstack/react-query';
import { product, type ProductsListSource } from '@salla.sa/twilight-theme-engine/api/product';
function ProductsSlider({ source, sourceValue }: { source?: string; sourceValue?: number[] }) {
const { data: result, isSuccess } = useQuery({
...product.queries.list({ source: source as ProductsListSource, sourceValue }),
enabled: !!source,
});
if (!isSuccess) return null;
return (
<ul>
{result.items.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}useQuery loads in the browser. Use useSuspenseQuery instead when the data should also be in the server HTML.
Which one to use
| You need | Use |
|---|---|
| Data the page cannot render without | Route loader + API module |
| Extra data for one block, after paint | useQuery with module.queries |
| Data that changes on a click (cart, wishlist) | Salla SDK |
Next: Layout and components, to change how pages look.