Reading and writing data
Reads go through loaders, the api modules and the query cache; writes go through window.Salla and come back as SDK events.
Data moves in two directions, on two different roads.
- Reading (products, categories, the menu) happens before the page is drawn, in a loader, and usually on the server. The result travels to the browser inside the HTML.
- Writing (add to cart, wishlist, coupons) happens in the browser, after a click, through the Salla SDK. The result comes back as an event.
The top rows of the diagram are the reading road; the bottom row is the writing road.
1. A loader asks for the data
Before a page is drawn, its loader (a function that runs before the page) asks for what the page shows. For the first page it runs on the server; after that, in the browser. It uses the engine's ready-made functions, such as product.list().
In engine terms
The generated route calls the module loader, e.g. Product.loader → productLoader → product.findOrThrow(id) (src/api/product.ts). Other loaders go through the QueryClient: homeLoader calls queryClient.ensureQueryData(home.queries.components()), the listing loaders queryClient.fetchQuery(product.queries.list(…)), and the root beforeLoad ensureQueryData(store.queries.settings()).
Reading: loader first, component second
import { createFileRoute } from '@tanstack/react-router';
import { useQuery } from '@tanstack/react-query';
import { getTwilightContext } from '@salla.sa/twilight-theme-engine/tanstack';
import { product } from '@salla.sa/twilight-theme-engine/api/product';
const latest = () => product.queries.list({ source: 'latest', perPage: 8 });
// Declared in app/routes.ts as route('/lookbook', 'lookbook.tsx').
export const Route = createFileRoute('/{-$locale}/lookbook')({
// Read in the loader: the products are in the server HTML.
loader: async () => {
await getTwilightContext().queryClient.ensureQueryData(latest());
return { page: { slug: 'lookbook', title: 'Lookbook' } };
},
component: Lookbook,
});
function Lookbook() {
// Same query key: answered from the cache the loader filled, no second request.
const { data } = useQuery(latest());
return <ul>{data?.items.map((item) => <li key={item.id}>{item.name}</li>)}</ul>;
}
What this page has cached
This is the query cache of the tab you are reading. Keys starting with store and translations were filled on the server by the root route and arrived with the HTML; the rest came from demos and pages you opened in this tab.
Reading the cache…
Writing: through the SDK, back as an event
import { useEffect } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { getSallaSDK } from '@salla.sa/twilight-theme-engine/utils';
// Write: in a click handler, through the SDK.
export function QuickAdd({ productId }: { productId: number }) {
return (
<button type="button" onClick={() => void getSallaSDK()?.cart.addItem(productId, 1)}>
Add to cart
</button>
);
}
// React to the write: nothing refreshes your own queries for you.
export function useRefreshCartQueries() {
const queryClient = useQueryClient();
useEffect(() => {
const refresh = () => void queryClient.invalidateQueries({ queryKey: ['cart'] });
let sdk = getSallaSDK();
let active = true;
void sdk?.onReady().then(() => {
sdk = getSallaSDK();
if (active) sdk?.event.on('cart::updated', refresh);
});
return () => {
active = false;
sdk?.event?.off('cart::updated', refresh);
};
}, [queryClient]);
}
Try it: press Add to cart on a card below (it adds to your own guest cart on the demo store), then watch the event log under it.
Loading products…
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 LatestProducts() {
const { data } = useQuery(product.queries.list({ source: 'latest', perPage: 8 }));
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}
imagePriority={index < 2}
/>
))}
</div>
);
}
Waiting for the SDK…
Why it matters
- Data fetched in `useEffect` is missing from the server HTML: the shopper sees a spinner first, and search engines may see nothing. Read it in the loader and share it through a query key.
- `window.Salla` in a loader throws on the server, where the first page is loaded. Keep SDK calls in event handlers and effects.
- The screen does not change after a write you made yourself: the engine does not refresh your queries. Listen for the SDK event and invalidate them, or use a hook that already listens, such as
useWishlist. - A request answered for the wrong store or language means the header came from somewhere else: always call Salla through the engine's
apimodules, not a barefetch.
Go deeper: the api client, product.list, shouldDehydrateQuery, getSallaSDK, useWishlist and route modules.